From 0a104ab4b50165d7088c0d9533d8bbb2ae6aab11 Mon Sep 17 00:00:00 2001
From: Jan Zickermann <jan.zickermann@dataport.de>
Date: Tue, 18 Feb 2025 17:34:56 +0100
Subject: [PATCH] OZG-4097 receive-attachment: Persist attachments before
 mapping

---
 .../attachment/Osi2AttachmentFileMapper.java  |  4 +
 .../attachment/Osi2AttachmentFileService.java | 13 +--
 .../Osi2PersistAttachmentService.java         | 52 ++++++++++
 .../postfach/osiv2/model/AttachmentInfo.java  | 12 +++
 .../postfach/osiv2/model/FileChunkInfo.java   |  4 +-
 ...si2FileUpload.java => Osi2Attachment.java} |  8 +-
 .../postfach/osiv2/model/Osi2Message.java     | 21 ++++
 .../osiv2/transfer/Osi2MessageMapper.java     | 26 +++++
 .../osiv2/transfer/Osi2PostfachService.java   | 18 ++--
 .../osiv2/transfer/Osi2QuarantineService.java | 32 +++---
 .../osiv2/transfer/Osi2RequestMapper.java     | 10 +-
 .../osiv2/transfer/Osi2ResponseMapper.java    | 40 ++++----
 .../transfer/PostfachApiFacadeService.java    | 15 ++-
 .../osiv2/OsiPostfachRemoteServiceITCase.java | 14 +--
 .../osiv2/OsiPostfachRemoteServiceTest.java   |  4 +-
 .../exception/Osi2ExceptionHandlerTest.java   |  4 +-
 .../AttachmentExampleUploadUtil.java          |  2 +-
 ...sageExchangeReceiveMessageTestFactory.java | 16 ++-
 ...xchangeSendMessageResponseTestFactory.java | 12 ++-
 .../factory/Osi2FileUploadTestFactory.java    | 10 +-
 .../osiv2/factory/Osi2MessageTestFactory.java | 25 +++++
 ...ploadTest.java => Osi2AttachmentTest.java} |  8 +-
 .../osiv2/transfer/Osi2MessageMapperTest.java | 98 +++++++++++++++++++
 .../transfer/Osi2PostfachServiceTest.java     | 26 ++---
 .../transfer/Osi2QuarantineServiceTest.java   | 26 ++---
 .../osiv2/transfer/Osi2RequestMapperTest.java |  8 +-
 .../transfer/Osi2ResponseMapperTest.java      | 84 ++++------------
 .../PostfachApiFacadeServiceTest.java         | 21 ++--
 28 files changed, 417 insertions(+), 196 deletions(-)
 create mode 100644 src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentService.java
 create mode 100644 src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentInfo.java
 rename src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/{Osi2FileUpload.java => Osi2Attachment.java} (66%)
 create mode 100644 src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Message.java
 create mode 100644 src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapper.java
 create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2MessageTestFactory.java
 rename src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/{Osi2FileUploadTest.java => Osi2AttachmentTest.java} (87%)
 create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapperTest.java

diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapper.java
index 35ac4d0..436a70e 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapper.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapper.java
@@ -8,6 +8,7 @@ import de.ozgcloud.apilib.file.OzgCloudFile;
 import de.ozgcloud.apilib.file.OzgCloudFileId;
 import de.ozgcloud.apilib.file.OzgCloudUploadFile;
 import de.ozgcloud.nachrichten.file.AttachmentFile;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo;
 import de.ozgcloud.vorgang.grpc.file.GrpcOzgFile;
 
 @Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
@@ -24,4 +25,7 @@ public interface Osi2AttachmentFileMapper {
 	@Mapping(target = "fileName", source = "name")
 	@Mapping(target = "fieldName", constant = ATTACHMENT_FIELD_NAME)
 	OzgCloudUploadFile toOzgCloudUploadFile(AttachmentFile attachmentFile);
+
+	AttachmentFile createAttachmentFile(AttachmentInfo attachment, String vorgangId);
+
 }
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileService.java
index 355f468..311c84e 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileService.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileService.java
@@ -13,7 +13,6 @@ import de.ozgcloud.apilib.file.OzgCloudFile;
 import de.ozgcloud.apilib.file.OzgCloudFileService;
 import de.ozgcloud.nachrichten.file.AttachmentFile;
 import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled;
-import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException;
 import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc;
 import de.ozgcloud.vorgang.grpc.binaryFile.GrpcBinaryFilesRequest;
 import de.ozgcloud.vorgang.grpc.binaryFile.GrpcGetBinaryFileDataRequest;
@@ -33,14 +32,6 @@ public class Osi2AttachmentFileService {
 	private final OzgCloudCallContextProvider ozgCloudCallContextProvider;
 
 	public List<OzgCloudFile> getFileMetadataOfIds(List<String> fileIds) {
-		try {
-			return getFileMetadataOfIdsRaw(fileIds);
-		} catch (RuntimeException e) {
-			throw new Osi2RuntimeException("getFileMetadataOfIds failed!", e);
-		}
-	}
-
-	List<OzgCloudFile> getFileMetadataOfIdsRaw(List<String> fileIds) {
 		return getBinaryFilServiceStub().findBinaryFilesMetaData(createRequestWithFileIds(fileIds))
 				.getFileList()
 				.stream()
@@ -48,10 +39,10 @@ public class Osi2AttachmentFileService {
 				.toList();
 	}
 
-	public String uploadFileAndReturnId(AttachmentFile attachmentFile, Supplier<InputStream> fileInputStream) {
+	public String uploadFileAndReturnId(AttachmentFile attachmentFile, InputStream fileInputStream) {
 		var attachmentFileId = ozgCloudFileService.uploadFile(
 				attachmentFileMapper.toOzgCloudUploadFile(attachmentFile),
-				fileInputStream.get()
+				fileInputStream
 		);
 		return attachmentFileId.toString();
 	}
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentService.java
new file mode 100644
index 0000000..d58e1ff
--- /dev/null
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentService.java
@@ -0,0 +1,52 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.attachment;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.springframework.core.io.Resource;
+
+import de.ozgcloud.nachrichten.file.AttachmentFile;
+import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled;
+import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
+import de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+@ServiceIfOsi2Enabled
+@RequiredArgsConstructor
+public class Osi2PersistAttachmentService {
+	private final PostfachApiFacadeService postfachApiFacadeService;
+	private final Osi2AttachmentFileService attachmentFileService;
+
+	public List<String> persistAttachments(Osi2Message osi2Message) {
+		return osi2Message.attachments().stream()
+				.map(info -> persistAttachment(osi2Message, info))
+				.toList();
+	}
+
+	String persistAttachment(Osi2Message osi2Message, AttachmentInfo attachment) {
+		return persistAttachmentFile(
+				buildAttachmentFile(osi2Message.vorgangId(), attachment),
+				postfachApiFacadeService.downloadAttachment(osi2Message.messageId(), attachment.guid())
+		);
+	}
+
+	AttachmentFile buildAttachmentFile(String vorgangId, AttachmentInfo attachment) {
+		return AttachmentFile.builder()
+				.name(attachment.name())
+				.contentType(attachment.contentType())
+				.vorgangId(vorgangId)
+				.build();
+	}
+
+	String persistAttachmentFile(AttachmentFile attachmentFile, Resource content) {
+		try (var inputStream = content.getInputStream()) {
+			return attachmentFileService.uploadFileAndReturnId(attachmentFile, inputStream);
+		} catch (IOException e) {
+			throw new Osi2RuntimeException("Error while persisting attachment", e);
+		}
+	}
+}
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentInfo.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentInfo.java
new file mode 100644
index 0000000..5d204b1
--- /dev/null
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentInfo.java
@@ -0,0 +1,12 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.model;
+
+import lombok.Builder;
+
+@Builder
+public record AttachmentInfo(
+		String guid,
+		String name,
+		String contentType,
+		Long size
+) {
+}
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java
index d6c6167..a119cba 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java
@@ -1,6 +1,6 @@
 package de.ozgcloud.nachrichten.postfach.osiv2.model;
 
-import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*;
+import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*;
 
 import java.io.InputStream;
 
@@ -11,7 +11,7 @@ import lombok.Builder;
 
 @Builder
 public record FileChunkInfo(
-		Osi2FileUpload upload,
+		Osi2Attachment upload,
 		long chunkIndex
 ) {
 	public AbstractResource createUploadResource(InputStream fileInputStream) {
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Attachment.java
similarity index 66%
rename from src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java
rename to src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Attachment.java
index b5bfd23..dc41ec7 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Attachment.java
@@ -6,14 +6,14 @@ import de.ozgcloud.apilib.file.OzgCloudFile;
 import lombok.Builder;
 
 @Builder
-public record Osi2FileUpload(
+public record Osi2Attachment(
 		String guid,
 		OzgCloudFile file
 ) {
 	public static final long CHUNK_SIZE = 100L * (2L << 10);
 
-	public static Osi2FileUpload from(OzgCloudFile file) {
-		return Osi2FileUpload.builder()
+	public static Osi2Attachment from(OzgCloudFile file) {
+		return Osi2Attachment.builder()
 				.guid(UUID.randomUUID().toString())
 				.file(file)
 				.build();
@@ -24,6 +24,6 @@ public record Osi2FileUpload(
 	}
 
 	public String getLoggableString() {
-		return String.format("FileUpload(guid=%s, ozgFileId=%s, size=%d)", guid, file.getId(), file.getSize());
+		return String.format("Attachment(messageGuid=%s, ozgFileId=%s, size=%d)", guid, file.getId(), file.getSize());
 	}
 }
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Message.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Message.java
new file mode 100644
index 0000000..f527fce
--- /dev/null
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Message.java
@@ -0,0 +1,21 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.model;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import de.ozgcloud.nachrichten.postfach.PostfachAddress;
+import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
+import lombok.Builder;
+
+@Builder
+public record Osi2Message(
+		String vorgangId,
+		PostfachAddress postfachAddress,
+		String messageId,
+		ZonedDateTime createdAt,
+		String subject,
+		String mailBody,
+		PostfachNachricht.ReplyOption replyOption,
+		List<AttachmentInfo> attachments
+) {
+}
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapper.java
new file mode 100644
index 0000000..7d81f6f
--- /dev/null
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapper.java
@@ -0,0 +1,26 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.transfer;
+
+import java.util.List;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.ReportingPolicy;
+
+import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
+
+@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
+public interface Osi2MessageMapper {
+	@Mapping(target = "id", ignore = true)
+	@Mapping(target = "referencedNachricht", ignore = true)
+
+	@Mapping(target = "createdBy", ignore = true)
+
+	@Mapping(target = "sentAt", ignore = true)
+	@Mapping(target = "sentSuccessful", ignore = true)
+	@Mapping(target = "messageCode", ignore = true)
+
+	@Mapping(target = "attachments", source = "attachmentIds")
+	@Mapping(target = "direction", constant = "IN")
+	PostfachNachricht toPostfachNachricht(Osi2Message osi2Message, List<String> attachmentIds);
+}
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java
index a53021e..754a945 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java
@@ -2,13 +2,9 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer;
 
 import java.util.stream.Stream;
 
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Scope;
-import org.springframework.stereotype.Service;
-
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
 import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled;
-import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties;
+import de.ozgcloud.nachrichten.postfach.osiv2.attachment.Osi2PersistAttachmentService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 
@@ -18,6 +14,8 @@ import lombok.extern.log4j.Log4j2;
 public class Osi2PostfachService {
 	private final PostfachApiFacadeService postfachApiFacadeService;
 	private final Osi2QuarantineService quarantineService;
+	private final Osi2PersistAttachmentService persistAttachmentService;
+	private final Osi2MessageMapper messageMapper;
 
 	public void sendMessage(PostfachNachricht nachricht) {
 		postfachApiFacadeService.sendMessage(
@@ -29,7 +27,15 @@ public class Osi2PostfachService {
 	public Stream<PostfachNachricht> receiveMessages() {
 		return postfachApiFacadeService.fetchPendingMessageIds()
 				.stream()
-				.map(postfachApiFacadeService::fetchMessageById);
+				.map(this::fetchPostfachNachricht);
+	}
+
+	PostfachNachricht fetchPostfachNachricht(String messageGuid) {
+		var message = postfachApiFacadeService.fetchMessageById(messageGuid);
+		return messageMapper.toPostfachNachricht(
+				message,
+				persistAttachmentService.persistAttachments(message)
+		);
 	}
 
 	public void deleteMessage(final String messageId) {
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java
index e2997b8..b36dc7f 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java
@@ -16,7 +16,7 @@ import de.ozgcloud.nachrichten.postfach.osiv2.attachment.Osi2AttachmentFileServi
 import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException;
 import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException;
 import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 
@@ -30,11 +30,11 @@ public class Osi2QuarantineService {
 	static final Duration POLLING_INTERVAL = Duration.ofSeconds(1);
 	static final Duration POLLING_TIMEOUT = Duration.ofMinutes(5);
 
-	public List<Osi2FileUpload> uploadAttachments(List<String> attachmentIds) {
+	public List<Osi2Attachment> uploadAttachments(List<String> attachmentIds) {
 		return uploadFiles(binaryFileService.getFileMetadataOfIds(attachmentIds));
 	}
 
-	List<Osi2FileUpload> uploadFiles(List<OzgCloudFile> ozgCloudFiles) {
+	List<Osi2Attachment> uploadFiles(List<OzgCloudFile> ozgCloudFiles) {
 		var sortedUploadFiles = deriveSortedUploadFiles(ozgCloudFiles);
 		try {
 			tryUploadSortedFiles(sortedUploadFiles);
@@ -45,23 +45,23 @@ public class Osi2QuarantineService {
 		}
 	}
 
-	List<Osi2FileUpload> deriveSortedUploadFiles(List<OzgCloudFile> ozgCloudFiles) {
+	List<Osi2Attachment> deriveSortedUploadFiles(List<OzgCloudFile> ozgCloudFiles) {
 		return ozgCloudFiles.stream()
 				.sorted(Comparator.comparing(OzgCloudFile::getSize).reversed())
-				.map(Osi2FileUpload::from)
+				.map(Osi2Attachment::from)
 				.toList();
 	}
 
-	void tryUploadSortedFiles(List<Osi2FileUpload> sortedUploadFiles) {
+	void tryUploadSortedFiles(List<Osi2Attachment> sortedUploadFiles) {
 		uploadFilesToQuarantine(sortedUploadFiles);
 		waitForVirusScan(sortedUploadFiles);
 	}
 
-	void uploadFilesToQuarantine(List<Osi2FileUpload> uploads) {
+	void uploadFilesToQuarantine(List<Osi2Attachment> uploads) {
 		uploads.forEach(this::uploadFileToQuarantine);
 	}
 
-	void uploadFileToQuarantine(Osi2FileUpload upload) {
+	void uploadFileToQuarantine(Osi2Attachment upload) {
 		try (var fileInputStream = binaryFileService.downloadFileContent(upload.file().getId().toString())) {
 			uploadInputStreamToQuarantine(upload, fileInputStream);
 		} catch (IOException e) {
@@ -69,7 +69,7 @@ public class Osi2QuarantineService {
 		}
 	}
 
-	void uploadInputStreamToQuarantine(Osi2FileUpload upload, InputStream fileInputStream) {
+	void uploadInputStreamToQuarantine(Osi2Attachment upload, InputStream fileInputStream) {
 		streamFileChunkInfos(upload)
 				.forEachOrdered(chunkInfo ->
 						postfachApiFacadeService.uploadChunk(
@@ -79,30 +79,30 @@ public class Osi2QuarantineService {
 				);
 	}
 
-	Stream<FileChunkInfo> streamFileChunkInfos(Osi2FileUpload upload) {
+	Stream<FileChunkInfo> streamFileChunkInfos(Osi2Attachment upload) {
 		return LongStream.range(0, upload.numberOfChunks() + 1)
 				.mapToObj(chunkIndex -> buildFileChunkInfo(upload, chunkIndex));
 	}
 
-	private FileChunkInfo buildFileChunkInfo(Osi2FileUpload upload, long chunkIndex) {
+	private FileChunkInfo buildFileChunkInfo(Osi2Attachment upload, long chunkIndex) {
 		return FileChunkInfo.builder()
 				.upload(upload)
 				.chunkIndex(chunkIndex)
 				.build();
 	}
 
-	void waitForVirusScan(List<Osi2FileUpload> osi2FileMetadata) {
+	void waitForVirusScan(List<Osi2Attachment> osi2FileMetadata) {
 		if (!waitUntil(() -> checkVirusScanCompleted(osi2FileMetadata), POLLING_INTERVAL, POLLING_TIMEOUT)) {
 			throw new Osi2RuntimeException("Expect the scan to complete after %d seconds!".formatted(POLLING_TIMEOUT.getSeconds()));
 		}
 	}
 
-	synchronized boolean checkVirusScanCompleted(List<Osi2FileUpload> osi2FileMetadata) {
+	synchronized boolean checkVirusScanCompleted(List<Osi2Attachment> osi2FileMetadata) {
 		return osi2FileMetadata.stream()
 				.allMatch(this::checkOneVirusScanCompleted);
 	}
 
-	boolean checkOneVirusScanCompleted(Osi2FileUpload osi2FileMetadata) {
+	boolean checkOneVirusScanCompleted(Osi2Attachment osi2FileMetadata) {
 		try {
 			return postfachApiFacadeService.checkUploadSuccessful(osi2FileMetadata.guid());
 		} catch (Osi2UploadException e) {
@@ -110,9 +110,9 @@ public class Osi2QuarantineService {
 		}
 	}
 
-	void deleteAttachments(List<Osi2FileUpload> uploads) {
+	void deleteAttachments(List<Osi2Attachment> uploads) {
 		uploads.stream()
-				.map(Osi2FileUpload::guid)
+				.map(Osi2Attachment::guid)
 				.forEach(postfachApiFacadeService::deleteFileUpload);
 	}
 
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java
index 286bd58..2f22828 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java
@@ -21,7 +21,7 @@ import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.OutSendMessageRequestV2;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1References;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyBehavior;
 import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
 
 @Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
 public interface Osi2RequestMapper {
@@ -38,9 +38,9 @@ public interface Osi2RequestMapper {
 	@Mapping(target = "isHtml", expression = "java( false )")
 	@Mapping(target = "files", expression = "java( mapMessageExchangeFiles(nachricht, files) )")
 	@Mapping(target = "references", expression = "java( mapReferences() )")
-	OutSendMessageRequestV2 mapOutSendMessageRequestV2(PostfachNachricht nachricht, List<Osi2FileUpload> files);
+	OutSendMessageRequestV2 mapOutSendMessageRequestV2(PostfachNachricht nachricht, List<Osi2Attachment> files);
 
-	default List<MessageExchangeFiles> mapMessageExchangeFiles(PostfachNachricht nachricht, List<Osi2FileUpload> files) {
+	default List<MessageExchangeFiles> mapMessageExchangeFiles(PostfachNachricht nachricht, List<Osi2Attachment> files) {
 		var filesById = associateUploadWithAttachmentId(files);
 		return nachricht.getAttachments()
 				.stream()
@@ -52,7 +52,7 @@ public interface Osi2RequestMapper {
 				.toList();
 	}
 
-	default Map<String, Osi2FileUpload> associateUploadWithAttachmentId(List<Osi2FileUpload> uploads) {
+	default Map<String, Osi2Attachment> associateUploadWithAttachmentId(List<Osi2Attachment> uploads) {
 		return uploads.stream()
 				.filter(upload -> upload.file() != null && upload.file().getId() != null)
 				.collect(Collectors.toMap(upload -> upload.file().getId().toString(), Function.identity()));
@@ -62,7 +62,7 @@ public interface Osi2RequestMapper {
 	@Mapping(target = "name", source = "file.name")
 	@Mapping(target = "size", source = "file.size")
 	@Mapping(target = "isOriginalMessage", expression = "java( false )")
-	MessageExchangeFiles mapMessageExchangeFile(Osi2FileUpload fileUpload);
+	MessageExchangeFiles mapMessageExchangeFile(Osi2Attachment fileUpload);
 
 	default List<V1References> mapReferences() {
 		return Collections.emptyList();
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java
index a578105..43a65de 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java
@@ -19,12 +19,16 @@ import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
 import de.ozgcloud.nachrichten.postfach.osiv2.OsiPostfachRemoteService;
 import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException;
 import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException;
+import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveAttachment;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessage;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessagesResponse;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineFileResult;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyBehavior;
+import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyFiles;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
 import lombok.Builder;
 import lombok.Getter;
 
@@ -34,31 +38,24 @@ public interface Osi2ResponseMapper {
 	String POSTFACH_ADDRESS_VERSION = "2.0";
 	int POSTFACH_ADDRESS_TYPE = 2;
 
-	@Mapping(target = "id", ignore = true)
-	@Mapping(target = "vorgangId", source = "sequencenumber")
-	@Mapping(target = "postfachAddress", source = "messageBox")
-	@Mapping(target = "messageId", source = "guid")
-	@Mapping(target = "referencedNachricht", ignore = true)
-
-	@Mapping(target = "createdAt", source = "responseTime", qualifiedByName = "mapOffsetDateTimeToZoned")
-	@Mapping(target = "createdBy", ignore = true)
-
-	@Mapping(target = "sentAt", ignore = true)
-	@Mapping(target = "sentSuccessful", ignore = true)
-	@Mapping(target = "messageCode", ignore = true)
-
-	@Mapping(target = "direction", constant = "IN")
-
+	@Mapping(target = "vorgangId", source = "message.sequencenumber")
+	@Mapping(target = "postfachAddress", source = "message.messageBox")
+	@Mapping(target = "messageId", source = "message.guid")
+	@Mapping(target = "createdAt", source = "message.responseTime", qualifiedByName = "mapOffsetDateTimeToZoned")
 	@Mapping(target = "subject", source = "subject")
 	@Mapping(target = "mailBody", source = ".", qualifiedByName = "mapMailBody")
 	@Mapping(target = "replyOption", source = "replyAction")
-	@Mapping(target = "attachments", ignore = true)
-	PostfachNachricht toPostfachNachricht(V1ReplyMessage message);
+	@Mapping(target = "attachments", source = "files")
+	Osi2Message toMessage(V1ReplyMessage message);
+
+	@Mapping(target = "contentType", source = "mimeType")
+	AttachmentInfo toAttachmentInfo(V1ReplyFiles attachment);
 
 	default String mapNullToEmpty(String value) {
 		return value == null ? "" : value;
 	}
 
+
 	@Named("mapOffsetDateTimeToZoned")
 	default ZonedDateTime mapOffsetDateTimeToZoned(OffsetDateTime offsetDateTime) {
 		return offsetDateTime == null ? null : offsetDateTime.toZonedDateTime();
@@ -125,6 +122,15 @@ public interface Osi2ResponseMapper {
 		}
 	}
 
+	@Named("mapMessageExchangeReceiveAttachment")
+	default String mapMessageExchangeReceiveAttachment(MessageExchangeReceiveAttachment attachment) {
+		return Optional.ofNullable(attachment)
+				.map(MessageExchangeReceiveAttachment::getGuid)
+				.map(UUID::toString)
+				.orElse(null);
+	}
+
+
 	@Builder
 	@Getter
 	class StringBasedIdentifier implements PostfachAddressIdentifier {
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java
index c38e730..0c1aed4 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java
@@ -6,6 +6,7 @@ import java.util.List;
 import java.util.UUID;
 
 import org.springframework.core.io.AbstractResource;
+import org.springframework.core.io.Resource;
 
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
 import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled;
@@ -14,7 +15,8 @@ import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi;
 import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 
@@ -29,7 +31,7 @@ public class PostfachApiFacadeService {
 	private final Osi2ResponseMapper responseMapper;
 	private final Osi2PostfachProperties.ApiConfiguration apiConfiguration;
 
-	public void sendMessage(PostfachNachricht nachricht, List<Osi2FileUpload> attachments) {
+	public void sendMessage(PostfachNachricht nachricht, List<Osi2Attachment> attachments) {
 		messageExchangeApi.sendMessage(
 				requestMapper.mapMailboxId(nachricht),
 				requestMapper.mapOutSendMessageRequestV2(nachricht, attachments)
@@ -57,11 +59,12 @@ public class PostfachApiFacadeService {
 		);
 	}
 
-	public PostfachNachricht fetchMessageById(final String messageId) {
+	public Osi2Message fetchMessageById(final String messageId) {
 		var messageReply = messageExchangeApi.getMessage(UUID.fromString(messageId));
-		return responseMapper.toPostfachNachricht(messageReply);
+		return responseMapper.toMessage(messageReply);
 	}
 
+
 	public void deleteMessage(final String messageId) {
 		messageExchangeApi.deleteMessage(UUID.fromString(messageId));
 	}
@@ -69,4 +72,8 @@ public class PostfachApiFacadeService {
 	public void deleteFileUpload(String fileUploadId) {
 		quarantineApi.deleteUpload(UUID.fromString(fileUploadId));
 	}
+
+	public Resource downloadAttachment(String messageId, String attachmentGuid) {
+		return messageExchangeApi.getMessageAttachment(UUID.fromString(messageId), UUID.fromString(attachmentGuid));
+	}
 }
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
index 82b3ca2..d2f0700 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
@@ -106,7 +106,7 @@ class OsiPostfachRemoteServiceITCase {
 						.success(true)
 						.build()))
 		);
-		postfachFacadeMockServer.stubFor(get(urlPathTemplate("/Quarantine/v1/Upload/{guid}"))
+		postfachFacadeMockServer.stubFor(get(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}"))
 				.willReturn(okJsonObj(QuarantineStatus.SAFE))
 		);
 		var postfachNachrichtWithAttachment = PostfachNachrichtTestFactory.createBuilder()
@@ -126,7 +126,7 @@ class OsiPostfachRemoteServiceITCase {
 		assertThat(requestBodyBytes.apply(1)).isEmpty();
 		postfachFacadeMockServer.verify(
 				exactly(1),
-				getRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{guid}"))
+				getRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}"))
 		);
 		postfachFacadeMockServer.verify(
 				exactly(1),
@@ -156,7 +156,7 @@ class OsiPostfachRemoteServiceITCase {
 						.error("Upload failure")
 						.build()))
 		);
-		postfachFacadeMockServer.stubFor(delete(urlPathTemplate("/Quarantine/v1/Upload/{guid}"))
+		postfachFacadeMockServer.stubFor(delete(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}"))
 				.willReturn(ok())
 		);
 		var postfachNachrichtWithAttachment = PostfachNachrichtTestFactory.createBuilder()
@@ -171,7 +171,7 @@ class OsiPostfachRemoteServiceITCase {
 
 		postfachFacadeMockServer.verify(
 				exactly(1),
-				deleteRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{guid}"))
+				deleteRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}"))
 		);
 	}
 
@@ -184,10 +184,10 @@ class OsiPostfachRemoteServiceITCase {
 						.success(true)
 						.build()))
 		);
-		postfachFacadeMockServer.stubFor(get(urlPathTemplate("/Quarantine/v1/Upload/{guid}"))
+		postfachFacadeMockServer.stubFor(get(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}"))
 				.willReturn(okJsonObj(QuarantineStatus.UNSAFE))
 		);
-		postfachFacadeMockServer.stubFor(delete(urlPathTemplate("/Quarantine/v1/Upload/{guid}"))
+		postfachFacadeMockServer.stubFor(delete(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}"))
 				.willReturn(ok())
 		);
 		var postfachNachrichtWithAttachment = PostfachNachrichtTestFactory.createBuilder()
@@ -202,7 +202,7 @@ class OsiPostfachRemoteServiceITCase {
 
 		postfachFacadeMockServer.verify(
 				exactly(1),
-				deleteRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{guid}"))
+				deleteRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}"))
 		);
 	}
 
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java
index afb85ea..5cb953e 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java
@@ -42,7 +42,7 @@ class OsiPostfachRemoteServiceTest {
 
 	@DisplayName("send message")
 	@Nested
-	class TestSendMessage {
+	class TestSendOsi2Message {
 
 		@DisplayName("should call send message")
 		@Test
@@ -95,7 +95,7 @@ class OsiPostfachRemoteServiceTest {
 
 	@DisplayName("delete message")
 	@Nested
-	class TestDeleteMessage {
+	class TestDeleteOsi2Message {
 
 		@DisplayName("should call deleteMessage")
 		@Test
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandlerTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandlerTest.java
index 8d0256f..14671a0 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandlerTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandlerTest.java
@@ -22,7 +22,7 @@ class Osi2ExceptionHandlerTest {
 
 	@DisplayName("derive message code")
 	@Nested
-	class TestDeriveMessageCode {
+	class TestDeriveOsi2MessageCode {
 		@Mock
 		RestClientResponseException restClientResponseException;
 
@@ -48,7 +48,7 @@ class Osi2ExceptionHandlerTest {
 
 	@DisplayName("derive message code from rest client response exception")
 	@Nested
-	class TestDeriveMessageCodeFromRestClientResponseException {
+	class TestDeriveOsi2MessageCodeFromRestClientResponseException {
 		@Mock
 		IOException ioException;
 
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/AttachmentExampleUploadUtil.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/AttachmentExampleUploadUtil.java
index bde195f..d8cb98e 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/AttachmentExampleUploadUtil.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/AttachmentExampleUploadUtil.java
@@ -20,7 +20,7 @@ public class AttachmentExampleUploadUtil {
 						.contentType("test/plain")
 						.name("test.txt")
 						.vorgangId(UUID.randomUUID().toString())
-				.build(), () -> new ByteArrayInputStream(EXAMPLE_TEXT_DATA));
+				.build(), new ByteArrayInputStream(EXAMPLE_TEXT_DATA));
 	}
 
 }
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java
index 75a60a2..278b052 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java
@@ -1,15 +1,25 @@
 package de.ozgcloud.nachrichten.postfach.osiv2.factory;
 
+import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory.*;
+
+import java.util.Arrays;
 import java.util.UUID;
 
+import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveAttachment;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessage;
 
 public class MessageExchangeReceiveMessageTestFactory {
 
-	public static final UUID MESSAGE_ID = UUID.randomUUID();
+	public static final String ATTACHMENT_1_ID = UUID.randomUUID().toString();
+	public static final String ATTACHMENT_2_ID = UUID.randomUUID().toString();
 
-	public static MessageExchangeReceiveMessage create() {
+	public static MessageExchangeReceiveMessage create(String... attachmentIds) {
 		return new MessageExchangeReceiveMessage()
-				.guid(MESSAGE_ID);
+				.guid(UUID.fromString(MESSAGE_ID))
+				.attachments(attachmentIds.length == 0 ? null : Arrays.stream(attachmentIds)
+						.map(attachmentId -> MessageExchangeReceiveAttachment.builder()
+								.guid(UUID.fromString(attachmentId))
+								.build())
+						.toList());
 	}
 }
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java
index 0c296a6..d0c543c 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java
@@ -1,13 +1,19 @@
 package de.ozgcloud.nachrichten.postfach.osiv2.factory;
 
-import static de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessageTestFactory.*;
+import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory.*;
+
+import java.util.UUID;
 
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeSendMessageResponse;
 
 public class MessageExchangeSendMessageResponseTestFactory {
 
 	public static MessageExchangeSendMessageResponse create() {
-		return new MessageExchangeSendMessageResponse()
-				.messageId(MESSAGE_ID);
+		return createBuilder().build();
+	}
+
+	public static MessageExchangeSendMessageResponse.Builder createBuilder() {
+		return MessageExchangeSendMessageResponse.builder()
+				.messageId(UUID.fromString(MESSAGE_ID));
 	}
 }
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2FileUploadTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2FileUploadTestFactory.java
index 1951488..a7e6153 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2FileUploadTestFactory.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2FileUploadTestFactory.java
@@ -1,12 +1,12 @@
 package de.ozgcloud.nachrichten.postfach.osiv2.factory;
 
-import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*;
+import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*;
 
 import java.util.UUID;
 
 import de.ozgcloud.apilib.file.OzgCloudFile;
 import de.ozgcloud.apilib.file.OzgCloudFileId;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
 
 public class Osi2FileUploadTestFactory {
 
@@ -18,12 +18,12 @@ public class Osi2FileUploadTestFactory {
 	public static final long NUMBER_OF_CHUNKS = 5;
 	public static final long UPLOAD_SIZE = CHUNK_SIZE * NUMBER_OF_CHUNKS;
 
-	public static Osi2FileUpload create() {
+	public static Osi2Attachment create() {
 		return createBuilder().build();
 	}
 
-	public static Osi2FileUpload.Osi2FileUploadBuilder createBuilder() {
-		return Osi2FileUpload.builder()
+	public static Osi2Attachment.Osi2AttachmentBuilder createBuilder() {
+		return Osi2Attachment.builder()
 				.guid(UPLOAD_GUID)
 				.file(OzgCloudFile.builder()
 						.id(new OzgCloudFileId(UPLOAD_FILE_ID))
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2MessageTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2MessageTestFactory.java
new file mode 100644
index 0000000..0192762
--- /dev/null
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2MessageTestFactory.java
@@ -0,0 +1,25 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.factory;
+
+import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*;
+
+import java.time.ZonedDateTime;
+
+import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
+
+public class Osi2MessageTestFactory {
+
+	public static Osi2Message create() {
+		return createBuilder().build();
+	}
+
+	public static Osi2Message.Osi2MessageBuilder createBuilder() {
+		return Osi2Message.builder()
+				.mailBody(MAIL_BODY)
+				.subject(MAIL_SUBJECT)
+				.replyOption(PostfachNachricht.ReplyOption.FORBIDDEN)
+				.createdAt(ZonedDateTime.now())
+				.vorgangId(VORGANG_ID)
+				.postfachAddress(PostfachAddressTestFactory.create());
+	}
+}
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUploadTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2AttachmentTest.java
similarity index 87%
rename from src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUploadTest.java
rename to src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2AttachmentTest.java
index 90dc14c..bbe1e1f 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUploadTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2AttachmentTest.java
@@ -1,6 +1,6 @@
 package de.ozgcloud.nachrichten.postfach.osiv2.model;
 
-import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*;
+import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*;
 import static org.assertj.core.api.Assertions.*;
 
 import org.junit.jupiter.api.DisplayName;
@@ -11,7 +11,7 @@ import org.junit.jupiter.params.provider.ValueSource;
 
 import de.ozgcloud.apilib.file.OzgCloudFileTestFactory;
 
-public class Osi2FileUploadTest {
+public class Osi2AttachmentTest {
 
 	@DisplayName("calculate number of chunks")
 	@Nested
@@ -49,8 +49,8 @@ public class Osi2FileUploadTest {
 			assertThat(result).isEqualTo(2L);
 		}
 
-		private Osi2FileUpload createWithSize(long size) {
-			return Osi2FileUpload.builder()
+		private Osi2Attachment createWithSize(long size) {
+			return Osi2Attachment.builder()
 					.file(OzgCloudFileTestFactory.createBuilder()
 							.size(size)
 							.build())
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapperTest.java
new file mode 100644
index 0000000..89d64ba
--- /dev/null
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapperTest.java
@@ -0,0 +1,98 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.transfer;
+
+import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*;
+import static java.util.Collections.*;
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+
+import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
+import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2MessageTestFactory;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
+
+class Osi2MessageMapperTest {
+
+	private final Osi2MessageMapper mapper = Mappers.getMapper(Osi2MessageMapper.class);
+
+	@DisplayName("to PostfachNachricht")
+	@Nested
+	class TestToPostfachNachricht {
+		private final Osi2Message osi2Message = Osi2MessageTestFactory.create();
+		private final String attachmentId1 = "attachmentId1";
+		private final String attachmentId2 = "attachmentId2";
+
+		@DisplayName("should map vorgang id")
+		@Test
+		void shouldMapVorgangId() {
+			var result = doMapping();
+
+			assertThat(result.getVorgangId()).isEqualTo(VORGANG_ID);
+		}
+
+		@DisplayName("should map mail body")
+		@Test
+		void shouldMapMailBody() {
+			var result = doMapping();
+
+			assertThat(result.getMailBody()).isEqualTo(MAIL_BODY);
+		}
+
+		@DisplayName("should map subject")
+		@Test
+		void shouldMapSubject() {
+			var result = doMapping();
+
+			assertThat(result.getSubject()).isEqualTo(MAIL_SUBJECT);
+		}
+
+		@DisplayName("should map reply option")
+		@Test
+		void shouldMapReplyOption() {
+			var result = doMapping();
+
+			assertThat(result.getReplyOption()).isEqualTo(PostfachNachricht.ReplyOption.FORBIDDEN);
+		}
+
+		@DisplayName("should map created at")
+		@Test
+		void shouldMapCreatedAt() {
+			var result = doMapping();
+
+			assertThat(result.getCreatedAt()).isEqualTo(osi2Message.createdAt());
+		}
+
+		@DisplayName("should map postfach address")
+		@Test
+		void shouldMapPostfachAddress() {
+			var result = doMapping();
+
+			assertThat(result.getPostfachAddress()).isEqualTo(osi2Message.postfachAddress());
+		}
+
+		@DisplayName("should map direction")
+		@Test
+		void shouldMapDirection() {
+			var result = doMapping();
+
+			assertThat(result.getDirection()).isEqualTo(PostfachNachricht.Direction.IN);
+		}
+
+		@DisplayName("should map attachments")
+		@Test
+		void shouldMapAttachments() {
+			var result = doMapping();
+
+			assertThat(result.getAttachments()).containsExactly(attachmentId1, attachmentId2);
+		}
+
+		private PostfachNachricht doMapping() {
+			return mapper.toPostfachNachricht(osi2Message, List.of(attachmentId1, attachmentId2));
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java
index 1a70dd9..7077d02 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java
@@ -13,16 +13,18 @@ import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.Spy;
 
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
 
 class Osi2PostfachServiceTest {
 
+	@Spy
 	@InjectMocks
-	private Osi2PostfachService osi2PostfachService;
+	private Osi2PostfachService service;
 
 	@Mock
 	private PostfachApiFacadeService postfachApiFacadeService;
@@ -31,9 +33,9 @@ class Osi2PostfachServiceTest {
 
 	@DisplayName("send message")
 	@Nested
-	class TestSendMessage {
+	class TestSendOsi2Message {
 		private final PostfachNachricht nachricht = PostfachNachrichtTestFactory.create();
-		private final List<Osi2FileUpload> uploadFiles = List.of(Osi2FileUploadTestFactory.create());
+		private final List<Osi2Attachment> uploadFiles = List.of(Osi2FileUploadTestFactory.create());
 
 		@BeforeEach
 		void mock() {
@@ -43,7 +45,7 @@ class Osi2PostfachServiceTest {
 		@DisplayName("should send message")
 		@Test
 		void shouldSendMessage() {
-			osi2PostfachService.sendMessage(nachricht);
+			service.sendMessage(nachricht);
 
 			verify(postfachApiFacadeService).sendMessage(nachricht, uploadFiles);
 		}
@@ -51,7 +53,7 @@ class Osi2PostfachServiceTest {
 		@DisplayName("should upload attachments of nachricht")
 		@Test
 		void shouldUploadAttachmentsOfNachricht() {
-			osi2PostfachService.sendMessage(nachricht);
+			service.sendMessage(nachricht);
 
 			verify(quarantineService).uploadAttachments(nachricht.getAttachments());
 		}
@@ -59,7 +61,7 @@ class Osi2PostfachServiceTest {
 
 	@DisplayName("receive messages")
 	@Nested
-	class TestReceiveMessage {
+	class TestReceiveOsi2Message {
 
 		@DisplayName("with two pending messages")
 		@Nested
@@ -68,10 +70,10 @@ class Osi2PostfachServiceTest {
 			@BeforeEach
 			void mock() {
 				when(postfachApiFacadeService.fetchPendingMessageIds()).thenReturn(List.of(MESSAGE_ID_1, MESSAGE_ID_2));
-				when(postfachApiFacadeService.fetchMessageById(MESSAGE_ID_1))
-						.thenReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_1).build());
-				when(postfachApiFacadeService.fetchMessageById(MESSAGE_ID_2))
-						.thenReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_2).build());
+				doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_1).build())
+						.when(service).fetchPostfachNachricht(MESSAGE_ID_1);
+				doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_2).build())
+						.when(service).fetchPostfachNachricht(MESSAGE_ID_2);
 			}
 
 			@DisplayName("should return")
@@ -87,7 +89,7 @@ class Osi2PostfachServiceTest {
 		}
 
 		private Stream<PostfachNachricht> receiveMessages() {
-			return osi2PostfachService.receiveMessages();
+			return service.receiveMessages();
 		}
 	}
 
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java
index 8e6e0a1..c411ff9 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java
@@ -1,7 +1,7 @@
 package de.ozgcloud.nachrichten.postfach.osiv2.transfer;
 
 import static de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory.*;
-import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*;
+import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*;
 import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2QuarantineService.*;
 import static org.assertj.core.api.Assertions.*;
 import static org.mockito.Mockito.*;
@@ -31,7 +31,7 @@ import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus;
 import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
 import lombok.SneakyThrows;
 
 class Osi2QuarantineServiceTest {
@@ -54,18 +54,18 @@ class Osi2QuarantineServiceTest {
 			.size(2000L)
 			.build();
 
-	private final Osi2FileUpload upload1 = Osi2FileUploadTestFactory.createBuilder()
+	private final Osi2Attachment upload1 = Osi2FileUploadTestFactory.createBuilder()
 			.file(file1)
 			.build();
-	private final Osi2FileUpload upload2 = Osi2FileUploadTestFactory.createBuilder()
+	private final Osi2Attachment upload2 = Osi2FileUploadTestFactory.createBuilder()
 			.guid(UUID.randomUUID().toString())
 			.file(file2)
 			.build();
-	private final Osi2FileUpload upload3 = Osi2FileUploadTestFactory.createBuilder()
+	private final Osi2Attachment upload3 = Osi2FileUploadTestFactory.createBuilder()
 			.guid(UUID.randomUUID().toString())
 			.build();
 
-	private final List<Osi2FileUpload> uploads = List.of(upload1, upload2, upload3);
+	private final List<Osi2Attachment> uploads = List.of(upload1, upload2, upload3);
 
 	@DisplayName("upload attachments")
 	@Nested
@@ -101,7 +101,7 @@ class Osi2QuarantineServiceTest {
 			assertThat(result).containsExactly(upload1);
 		}
 
-		List<Osi2FileUpload> uploadAttachments() {
+		List<Osi2Attachment> uploadAttachments() {
 			return service.uploadAttachments(List.of(UPLOAD_FILE_ID));
 		}
 	}
@@ -179,7 +179,7 @@ class Osi2QuarantineServiceTest {
 			}
 		}
 
-		List<Osi2FileUpload> uploadFiles() {
+		List<Osi2Attachment> uploadFiles() {
 			return service.uploadFiles(List.of(file1));
 		}
 	}
@@ -194,17 +194,17 @@ class Osi2QuarantineServiceTest {
 			var result = service.deriveSortedUploadFiles(List.of(file1, file2));
 
 			assertThat(result)
-					.extracting(Osi2FileUpload::file)
+					.extracting(Osi2Attachment::file)
 					.containsExactly(file2, file1);
 		}
 
-		@DisplayName("should have random upload guid")
+		@DisplayName("should have random upload messageGuid")
 		@Test
 		void shouldHaveRandomUploadGuid() {
 			var result = service.deriveSortedUploadFiles(List.of(file1, file2));
 
 			assertThat(result)
-					.extracting(Osi2FileUpload::guid)
+					.extracting(Osi2Attachment::guid)
 					.extracting(String::length)
 					.containsExactly(36, 36);
 		}
@@ -315,7 +315,7 @@ class Osi2QuarantineServiceTest {
 	@Nested
 	class TestStreamFileChunkInfos {
 
-		private final Osi2FileUpload upload = Osi2FileUploadTestFactory.createBuilder()
+		private final Osi2Attachment upload = Osi2FileUploadTestFactory.createBuilder()
 				.file(OzgCloudFileTestFactory.createBuilder()
 						.size(CHUNK_SIZE * 2)
 						.build())
@@ -374,7 +374,7 @@ class Osi2QuarantineServiceTest {
 	@DisplayName("check one virus scan completed")
 	@Nested
 	class TestCheckOneVirusScanCompleted {
-		private final Osi2FileUpload upload1 = Osi2FileUploadTestFactory.create();
+		private final Osi2Attachment upload1 = Osi2FileUploadTestFactory.create();
 
 		@DisplayName("should call checkUploadSuccessful")
 		@Test
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java
index 24040f0..f62ec8d 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java
@@ -79,7 +79,7 @@ class Osi2RequestMapperTest {
 
 	@DisplayName("map OutSendMessageRequestV2")
 	@Nested
-	class TestMapOutSendMessageRequestV2 {
+	class TestMapOutSendOsi2MessageRequestV2 {
 
 		@DisplayName("should map sequence number")
 		@Test
@@ -224,8 +224,8 @@ class Osi2RequestMapperTest {
 
 	@DisplayName("map message exchange file")
 	@Nested
-	class TestMapMessageExchangeFile {
-		@DisplayName("should map guid")
+	class TestMapOsi2MessageExchangeFile {
+		@DisplayName("should map messageGuid")
 		@Test
 		void shouldMapGuid() {
 			var result = mapFile();
@@ -268,7 +268,7 @@ class Osi2RequestMapperTest {
 
 		private final FileChunkInfo chunkInfo = FileChunkInfoTestFactory.create();
 
-		@DisplayName("should map upload guid")
+		@DisplayName("should map upload messageGuid")
 		@Test
 		void shouldMapUploadGuid() {
 			var result = doMapping();
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java
index 6001100..cfd7d23 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java
@@ -19,13 +19,13 @@ import org.mockito.InjectMocks;
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
 import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException;
 import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException;
-import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.QuarantineFileResultTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyBehavior;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
 import lombok.SneakyThrows;
 
 class Osi2ResponseMapperTest {
@@ -34,22 +34,22 @@ class Osi2ResponseMapperTest {
 	private Osi2ResponseMapper mapper = Mappers.getMapper(Osi2ResponseMapper.class);
 	private final V1ReplyMessage message = V1ReplyMessageTestFactory.create();
 
-	@DisplayName("map V1ReplyMessage to PostfachNachricht")
+	@DisplayName("map V1ReplyMessage to Osi2Message")
 	@Nested
-	class V1ReplyMessageToPostfachNachricht {
+	class V1ReplyOsi2MessageToOsi2Message {
 
 		@Test
 		void shouldMapVorgangId() {
 			var result = doMapping();
 
-			assertThat(result.getVorgangId()).isEqualTo(SEQUENCE_NUMMER);
+			assertThat(result.vorgangId()).isEqualTo(SEQUENCE_NUMMER);
 		}
 
 		@Test
 		void shouldMapPostfachAddress() {
 			var result = doMapping();
 
-			assertThat(result.getPostfachAddress().getIdentifier())
+			assertThat(result.postfachAddress().getIdentifier())
 					.hasToString(MESSAGE_BOX_ID);
 		}
 
@@ -57,28 +57,21 @@ class Osi2ResponseMapperTest {
 		void shouldMapCreatedAt() {
 			var result = doMapping();
 
-			assertThat(result.getCreatedAt()).isEqualTo(RESPONSE_TIME);
-		}
-
-		@Test
-		void shouldMapDirection() {
-			var result = doMapping();
-
-			assertThat(result.getDirection()).isEqualTo(PostfachNachricht.Direction.IN);
+			assertThat(result.createdAt()).isEqualTo(RESPONSE_TIME);
 		}
 
 		@Test
 		void shouldMapSubject() {
 			var result = doMapping();
 
-			assertThat(result.getSubject()).isEqualTo(SUBJECT);
+			assertThat(result.subject()).isEqualTo(SUBJECT);
 		}
 
 		@Test
 		void shouldMapNullBodyToEmptyString() {
 			var result = doMapping();
 
-			assertThat(result.getMailBody()).isEmpty();
+			assertThat(result.mailBody()).isEmpty();
 		}
 
 		@DisplayName("should map modified HTML body if HTML message")
@@ -88,9 +81,9 @@ class Osi2ResponseMapperTest {
 					.body(HTML_REPLY_BODY)
 					.isHtml(true);
 
-			var result = mapper.toPostfachNachricht(htmlMessage);
+			var result = mapper.toMessage(htmlMessage);
 
-			assertThat(result.getMailBody()).isEqualTo(REPLY_BODY);
+			assertThat(result.mailBody()).isEqualTo(REPLY_BODY);
 		}
 
 		@DisplayName("should map unmodified body if not HTML message")
@@ -101,9 +94,9 @@ class Osi2ResponseMapperTest {
 					.body(body)
 					.isHtml(false);
 
-			var result = mapper.toPostfachNachricht(htmlMessage);
+			var result = mapper.toMessage(htmlMessage);
 
-			assertThat(result.getMailBody()).isEqualTo(body);
+			assertThat(result.mailBody()).isEqualTo(body);
 		}
 
 		static Stream<Arguments> replyOptionValues() {
@@ -122,9 +115,9 @@ class Osi2ResponseMapperTest {
 					.replyAction(replyAction)
 					.isHtml(false);
 
-			var result = mapper.toPostfachNachricht(replyActionMessage);
+			var result = mapper.toMessage(replyActionMessage);
 
-			assertThat(result.getReplyOption()).isEqualTo(expected);
+			assertThat(result.replyOption()).isEqualTo(expected);
 		}
 
 		@DisplayName("should map messageId")
@@ -132,7 +125,7 @@ class Osi2ResponseMapperTest {
 		void shouldMapMessageId() {
 			var result = doMapping();
 
-			assertThat(result.getMessageId()).isEqualTo(MESSAGE_ID);
+			assertThat(result.messageId()).isEqualTo(MESSAGE_ID);
 		}
 
 		@DisplayName("should not fail if all fields are null")
@@ -140,7 +133,7 @@ class Osi2ResponseMapperTest {
 		void shouldNotFailIfAllFieldsAreNull() {
 			var nullMessage = new V1ReplyMessage();
 
-			assertThatCode(() -> mapper.toPostfachNachricht(nullMessage))
+			assertThatCode(() -> mapper.toMessage(nullMessage))
 					.doesNotThrowAnyException();
 		}
 
@@ -150,51 +143,12 @@ class Osi2ResponseMapperTest {
 			var nullMessage = new V1ReplyMessage()
 					.isHtml(true);
 
-			assertThatCode(() -> mapper.toPostfachNachricht(nullMessage))
+			assertThatCode(() -> mapper.toMessage(nullMessage))
 					.doesNotThrowAnyException();
 		}
 
-		private PostfachNachricht doMapping() {
-			return mapper.toPostfachNachricht(message);
-		}
-	}
-
-	@DisplayName("to message ids")
-	@Nested
-	class TestToMessageIds {
-
-		@DisplayName("should throw on null response")
-		@Test
-		void shouldThrowOnNullResponse() {
-			assertThatThrownBy(() -> mapper.toMessageIds(null))
-					.isInstanceOf(Osi2RuntimeException.class);
-		}
-
-		@DisplayName("should map empty response")
-		@Test
-		void shouldMapEmptyResponse() {
-			var response = MessageExchangeReceiveMessagesResponseTestFactory.create();
-
-			var result = mapper.toMessageIds(response);
-
-			assertThat(result).isEmpty();
-		}
-
-		@DisplayName("should map message ids")
-		@Test
-		void shouldMapMessageIds() {
-			var response = MessageExchangeReceiveMessagesResponseTestFactory.create(
-					MessageExchangeReceiveMessagesResponseTestFactory.MESSAGE_ID_1,
-					null,
-					MessageExchangeReceiveMessagesResponseTestFactory.MESSAGE_ID_2
-			);
-
-			var result = mapper.toMessageIds(response);
-
-			assertThat(result).containsExactly(
-					MessageExchangeReceiveMessagesResponseTestFactory.MESSAGE_ID_1,
-					MessageExchangeReceiveMessagesResponseTestFactory.MESSAGE_ID_2
-			);
+		private Osi2Message doMapping() {
+			return mapper.toMessage(message);
 		}
 	}
 
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java
index 95a4ab6..15c944a 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java
@@ -23,6 +23,7 @@ import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.FileChunkInfoTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory;
+import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2MessageTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi;
@@ -34,7 +35,7 @@ import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.OutSendMessageRequestV2;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage;
 import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
 import lombok.SneakyThrows;
 
 class PostfachApiFacadeServiceTest {
@@ -60,7 +61,7 @@ class PostfachApiFacadeServiceTest {
 
 	@DisplayName("send message")
 	@Nested
-	class TestSendMessage {
+	class TestSendOsi2Message {
 
 		@Mock
 		OutSendMessageRequestV2 outSendMessageRequestV2;
@@ -68,7 +69,7 @@ class PostfachApiFacadeServiceTest {
 		@Mock
 		MessageExchangeSendMessageResponse messageExchangeSendMessageResponse;
 
-		private final List<Osi2FileUpload> files = List.of(Osi2FileUploadTestFactory.create());
+		private final List<Osi2Attachment> files = List.of(Osi2FileUploadTestFactory.create());
 
 		private final PostfachNachricht nachricht = PostfachNachrichtTestFactory.create();
 
@@ -107,7 +108,7 @@ class PostfachApiFacadeServiceTest {
 
 	@DisplayName("fetch pending message ids")
 	@Nested
-	class TestFetchPendingMessageIds {
+	class TestFetchPendingOsi2MessageIds {
 
 		@DisplayName("with two pending messages")
 		@Nested
@@ -193,21 +194,21 @@ class PostfachApiFacadeServiceTest {
 		@Test
 		void shouldCallResponseMapper() {
 			when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage);
-			when(osi2ResponseMapper.toPostfachNachricht(any())).thenReturn(PostfachNachrichtTestFactory.create());
+			when(osi2ResponseMapper.toMessage(any())).thenReturn(Osi2MessageTestFactory.create());
 
 			service.fetchMessageById(MESSAGE_ID_1);
 
-			verify(osi2ResponseMapper).toPostfachNachricht(any());
+			verify(osi2ResponseMapper).toMessage(any());
 		}
 
 		@Test
 		void shouldReturnPostfachNachricht() {
 			when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage);
-			when(osi2ResponseMapper.toPostfachNachricht(any())).thenReturn(PostfachNachrichtTestFactory.create());
+			when(osi2ResponseMapper.toMessage(any())).thenReturn(Osi2MessageTestFactory.create());
 
-			var postfachNachricht = service.fetchMessageById(MESSAGE_ID_1);
+			var message = service.fetchMessageById(MESSAGE_ID_1);
 
-			assertThat(postfachNachricht).isInstanceOf(PostfachNachricht.class);
+			assertThat(message).isEqualTo(Osi2MessageTestFactory.create());
 		}
 	}
 
@@ -293,7 +294,7 @@ class PostfachApiFacadeServiceTest {
 
 	@DisplayName("delete message")
 	@Nested
-	class TestDeleteMessage {
+	class TestDeleteOsi2Message {
 		@Mock
 		MessageExchangeDeleteMessageResponse replyMessage;
 
-- 
GitLab