diff --git a/nachrichten-manager-interface/pom.xml b/nachrichten-manager-interface/pom.xml
index e2741639bd1a779001d0cf6f84563a71f70702e9..525544d7ea16d624325341b9ecc01d87b52e7e4c 100644
--- a/nachrichten-manager-interface/pom.xml
+++ b/nachrichten-manager-interface/pom.xml
@@ -31,7 +31,7 @@
 	<parent>
 		<groupId>de.ozgcloud.common</groupId>
 		<artifactId>ozgcloud-common-dependencies</artifactId>
-		<version>4.5.0</version>
+		<version>4.7.0</version>
 		<relativePath/>
 	</parent>
 
diff --git a/nachrichten-manager-server/pom.xml b/nachrichten-manager-server/pom.xml
index 2983ff7d66ceb26c05ae5a31cbbcd880240d36ae..d7ea12760090097fa41473c9f2e81b397f125ccf 100644
--- a/nachrichten-manager-server/pom.xml
+++ b/nachrichten-manager-server/pom.xml
@@ -48,8 +48,8 @@
 		<bayernid-proxy-interface.version>0.7.0</bayernid-proxy-interface.version>
 		<vorgang-manager.version>2.17.0</vorgang-manager.version>
 		<muk-postfach.version>0.1.0</muk-postfach.version>
-		<api-lib.version>0.13.0</api-lib.version>
-		<ozgcloud-common.version>4.5.0</ozgcloud-common.version>
+		<api-lib.version>0.16.0-SNAPSHOT</api-lib.version>
+		<ozgcloud-common.version>4.7.0</ozgcloud-common.version>
 	</properties>
 
 	<dependencies>
@@ -241,6 +241,10 @@
 			<type>test-jar</type>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>de.ozgcloud.common</groupId>
+			<artifactId>ozgcloud-common-lib</artifactId>
+		</dependency>
 	</dependencies>
 
 	<build>
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerCallContextProvider.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerCallContextProvider.java
index c64af3e512b11fece4881f0875c96b738561622d..fcf82624f923b069927a5aa7a70c65451905b77c 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerCallContextProvider.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerCallContextProvider.java
@@ -4,6 +4,7 @@ import org.springframework.stereotype.Component;
 
 import de.ozgcloud.apilib.common.callcontext.CallContext;
 import de.ozgcloud.apilib.common.callcontext.OzgCloudCallContextProvider;
+import de.ozgcloud.apilib.vorgang.OzgCloudUserIdMapper;
 import de.ozgcloud.nachrichten.common.grpc.NachrichtenCallContextAttachingInterceptor;
 import lombok.RequiredArgsConstructor;
 
@@ -11,8 +12,11 @@ import lombok.RequiredArgsConstructor;
 @RequiredArgsConstructor
 class NachrichtenManagerCallContextProvider implements OzgCloudCallContextProvider {
 
+	private final OzgCloudUserIdMapper userIdMapper;
+
 	@Override
 	public CallContext provideContext() {
-		return CallContext.builder().clientName(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_CLIENT_NAME).build();
+		return CallContext.builder().clientName(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_CLIENT_NAME)
+				.userId(userIdMapper.toUserId(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_SENDER_USER_ID)).build();
 	}
 }
\ No newline at end of file
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerConfiguration.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerConfiguration.java
index 7f8d749fcc4f46c569fdc72071ebf779a13282d2..48d588ffcd8ed11533b5b5319393ecb4adf5c715 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerConfiguration.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/NachrichtenManagerConfiguration.java
@@ -1,5 +1,7 @@
 package de.ozgcloud.nachrichten;
 
+import jakarta.activation.MimetypesFileTypeMap;
+
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -24,6 +26,8 @@ public class NachrichtenManagerConfiguration {
 	public static final String NACHRICHTEN_VORGANG_REMOTE_SERVICE = "nachrichten_vorgangRemoteService";
 	public static final String NACHRICHTEN_ATTACHED_ITEM_SERVICE = "nachrichten_attachedItemService";
 	public static final String NACHRICHTEN_OZG_CLOUD_FILE_MAPPER = "nachrichten_OzgCloudFileMapperImpl";
+	public static final String ATTACHMENT_FILE_SERVICE_NAME = "nachrichten_AttachmentFileService";
+	public static final String ATTACHMENT_FILE_MAPPER_NAME = "nachrichten_AttachmentFileMapper";
 
 	public static final String GRPC_VORGANG_MANAGER_NAME = "vorgang-manager";
 	public static final String GRPC_COMMAND_MANAGER_NAME = "command-manager";
@@ -47,4 +51,10 @@ public class NachrichtenManagerConfiguration {
 	OzgCloudFileService grpcOzgCloudFileService(NachrichtenManagerCallContextProvider contextProvider, OzgCloudFileMapper mapper) {
 		return new GrpcOzgCloudFileService(fileServiceBlockingStub, fileServiceAsyncServiceStub, contextProvider, mapper);
 	}
+
+	@Bean
+	MimetypesFileTypeMap mimetypesFileTypeMap() {
+		// uses map file: src/main/resources/mime.types
+		return new MimetypesFileTypeMap();
+	}
 }
\ No newline at end of file
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/NachrichtenCallContextAttachingInterceptor.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/NachrichtenCallContextAttachingInterceptor.java
index d3fa03da97bf23b79dfc2d31325b28777260d8be..90f7c91aa41ea10460a606281743bb1d6f47d2ef 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/NachrichtenCallContextAttachingInterceptor.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/NachrichtenCallContextAttachingInterceptor.java
@@ -23,10 +23,11 @@
  */
 package de.ozgcloud.nachrichten.common.grpc;
 
-import static de.ozgcloud.common.grpc.GrpcUtil.*;
-
 import java.util.UUID;
 
+import org.apache.logging.log4j.ThreadContext;
+
+import de.ozgcloud.common.grpc.GrpcUtil;
 import io.grpc.CallOptions;
 import io.grpc.Channel;
 import io.grpc.ClientCall;
@@ -37,12 +38,10 @@ import io.grpc.MethodDescriptor;
 
 public class NachrichtenCallContextAttachingInterceptor implements ClientInterceptor {
 
-	static final String KEY_USER_ID = "USER_ID-bin";
-	static final String KEY_CLIENT_NAME = "CLIENT_NAME-bin";
-	static final String KEY_REQUEST_ID = "REQUEST_ID-bin";
+	static final String REQUEST_ID_KEY = "requestId";
 
 	public static final String NACHRICHTEN_MANAGER_CLIENT_NAME = "OzgCloud_NachrichtenManager";
-	static final String NACHRICHTEN_MANAGER_SENDER_USER_ID = "system-nachrichten_manager-sender";
+	public static final String NACHRICHTEN_MANAGER_SENDER_USER_ID = "system-nachrichten_manager-sender";
 
 	// <A> = Request, <B> = Response
 	@Override
@@ -52,7 +51,7 @@ public class NachrichtenCallContextAttachingInterceptor implements ClientInterce
 
 	final class CallContextAttachingClientCall<A, B> extends SimpleForwardingClientCall<A, B> {
 
-		protected CallContextAttachingClientCall(ClientCall<A, B> delegate) {
+		CallContextAttachingClientCall(ClientCall<A, B> delegate) {
 			super(delegate);
 		}
 
@@ -62,19 +61,18 @@ public class NachrichtenCallContextAttachingInterceptor implements ClientInterce
 			super.start(responseListener, headers);
 		}
 
-		private Metadata buildCallContextMetadata() {
+		Metadata buildCallContextMetadata() {
 			var metadata = new Metadata();
 
-			metadata.put(createKeyOf(KEY_USER_ID), NACHRICHTEN_MANAGER_SENDER_USER_ID.getBytes());
-			metadata.put(createKeyOf(KEY_CLIENT_NAME), NACHRICHTEN_MANAGER_CLIENT_NAME.getBytes());
-			metadata.put(createKeyOf(KEY_REQUEST_ID), generateRequestId().getBytes());
+			metadata.put(GrpcUtil.HEADER_KEY_USER_ID, NACHRICHTEN_MANAGER_SENDER_USER_ID.getBytes());
+			metadata.put(GrpcUtil.HEADER_KEY_CLIENT_NAME, NACHRICHTEN_MANAGER_CLIENT_NAME.getBytes());
+			metadata.put(GrpcUtil.HEADER_KEY_REQUEST_ID, getRequestId());
 
 			return metadata;
 		}
 
-		// TODO OZG-1974 requestId zentraler erzeugen
-		private String generateRequestId() {
-			return UUID.randomUUID().toString();
+		byte[] getRequestId() {
+			return ThreadContext.getContext().getOrDefault(REQUEST_ID_KEY, UUID.randomUUID().toString()).getBytes();
 		}
 
 	}
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/BinaryFileService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFile.java
similarity index 71%
rename from nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/BinaryFileService.java
rename to nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFile.java
index 6f7465cea0ab0426c3ddcf8f456a159479905a53..dacbc05680f126479548000d5c7b709595f13855 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/BinaryFileService.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFile.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
  * Ministerpräsidenten des Landes Schleswig-Holstein
  * Staatskanzlei
  * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
@@ -21,16 +21,17 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-package de.ozgcloud.nachrichten.postfach;
+package de.ozgcloud.nachrichten.file;
 
-import java.io.InputStream;
+import lombok.Builder;
+import lombok.Getter;
 
-import com.mongodb.client.gridfs.model.GridFSFile;
+@Builder(toBuilder = true)
+@Getter
+public class AttachmentFile {
 
-//Temporally replacement for using GRPC Api
-public interface BinaryFileService {
+	private String name;
+	private String contentType;
+	private String vorgangId;
 
-	InputStream getUploadedFileStream(FileId fileId);
-
-	GridFSFile getFile(FileId fileId);
 }
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFileMapper.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFileMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ccc7ebe01892bc8bb3b158fc3ea5c9da8ded8f2
--- /dev/null
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFileMapper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.nachrichten.file;
+
+import org.mapstruct.AnnotateWith;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.NullValueCheckStrategy;
+import org.mapstruct.ReportingPolicy;
+import org.springframework.stereotype.Component;
+
+import de.ozgcloud.apilib.file.OzgCloudFile;
+import de.ozgcloud.apilib.file.OzgCloudUploadFile;
+import de.ozgcloud.nachrichten.NachrichtenManagerConfiguration;
+
+@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, unmappedTargetPolicy = ReportingPolicy.WARN)
+@AnnotateWith(value = Component.class, elements = @AnnotateWith.Element(strings = NachrichtenManagerConfiguration.ATTACHMENT_FILE_MAPPER_NAME))
+interface AttachmentFileMapper {
+
+	@Mapping(target = "vorgangId", ignore = true)
+	AttachmentFile fromOzgCloudFile(OzgCloudFile ozgCloudFile);
+
+	@Mapping(target = "fileName", source = "name")
+	@Mapping(target = "fieldName", constant = AttachmentFileService.ATTACHMENT_NAME)
+	OzgCloudUploadFile toOzgCloudUploadFile(AttachmentFile attachmentFile);
+}
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFileService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFileService.java
new file mode 100644
index 0000000000000000000000000000000000000000..6fc1895762c6ad4c3305bed30e3cb9d84853b217
--- /dev/null
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/file/AttachmentFileService.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.nachrichten.file;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.URLConnection;
+import java.util.Base64;
+import java.util.Optional;
+
+import jakarta.activation.MimetypesFileTypeMap;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.stereotype.Service;
+
+import de.ozgcloud.apilib.file.OzgCloudFileService;
+import de.ozgcloud.apilib.vorgang.OzgCloudFileIdMapper;
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.nachrichten.NachrichtenManagerConfiguration;
+import de.ozgcloud.nachrichten.postfach.FileId;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+@Service(NachrichtenManagerConfiguration.ATTACHMENT_FILE_SERVICE_NAME) // NOSONAR
+@RequiredArgsConstructor
+@Log4j2
+public class AttachmentFileService {
+
+	private static final int BUFFER_SIZE = 4 * 1024;
+	static final String ATTACHMENT_NAME = "PostfachAttachment";
+
+	@Qualifier(NachrichtenManagerConfiguration.OZG_CLOUD_FILE_SERVICE_NAME) // NOSONAR
+	private final OzgCloudFileService ozgCloudFileService;
+	private final OzgCloudFileIdMapper fileIdMapper;
+	@Qualifier(NachrichtenManagerConfiguration.ATTACHMENT_FILE_MAPPER_NAME) // NOSONAR
+	private final AttachmentFileMapper attachmentFileMapper;
+
+	private final TaskExecutor taskExecutor;
+
+	private final MimetypesFileTypeMap mimetypesFileTypeMap;
+
+	public AttachmentFile getFile(FileId fileId) {
+		var ozgCloudFile = ozgCloudFileService.getFile(fileIdMapper.toFileId(fileId.toString()));
+		return attachmentFileMapper.fromOzgCloudFile(ozgCloudFile);
+	}
+
+	public InputStream getFileContent(FileId fileId) {
+		var inputStream = createInputStream();
+		writeFileContent(fileId.toString(), connectToOutputStream(inputStream));
+		return inputStream;
+	}
+
+	PipedInputStream createInputStream() {
+		return new PipedInputStream(BUFFER_SIZE);
+	}
+
+	OutputStream connectToOutputStream(PipedInputStream inputStream) {
+		try {
+			return new PipedOutputStream(inputStream);
+		} catch (IOException e) {
+			throw new TechnicalException("Cannot read file content", e);
+		}
+	}
+
+	void writeFileContent(String fileId, OutputStream outputStream) {
+		taskExecutor.execute(() -> {
+			ozgCloudFileService.writeFileDataToStream(fileIdMapper.toFileId(fileId), outputStream);
+			IOUtils.closeQuietly(outputStream, e -> LOG.warn("Cannot close output stream", e));
+		});
+	}
+
+	public String uploadAttachmentFile(AttachmentFile attachmentFile, String fileContent) {
+		var decodedContent = Base64.getDecoder().decode(fileContent);
+		var ozgCloudFileId = ozgCloudFileService.uploadFile(
+				attachmentFileMapper.toOzgCloudUploadFile(addContentTypeIfMissing(attachmentFile, decodedContent)),
+				toInputStream(decodedContent));
+		return ozgCloudFileId.toString();
+	}
+
+	AttachmentFile addContentTypeIfMissing(AttachmentFile attachmentFile, byte[] fileContent) {
+		if (StringUtils.isBlank(attachmentFile.getContentType())) {
+			return attachmentFile.toBuilder().contentType(getContentType(attachmentFile, fileContent)).build();
+		}
+		return attachmentFile;
+	}
+
+	String getContentType(AttachmentFile attachmentFile, byte[] fileContent) {
+		return getTypeByFileName(attachmentFile).or(() -> getTypeByContent(fileContent)).orElseGet(() -> getByMimeTypes(attachmentFile));
+	}
+
+	Optional<String> getTypeByFileName(AttachmentFile attachmentFile) {
+		var contentType = URLConnection.getFileNameMap().getContentTypeFor(attachmentFile.getName());
+		return Optional.ofNullable(contentType);
+	}
+
+	Optional<String> getTypeByContent(byte[] fileContent) {
+		try (var contentStream = toInputStream(fileContent)) {
+			return Optional.ofNullable(URLConnection.guessContentTypeFromStream(contentStream));
+		} catch (IOException e) {
+			LOG.warn("IO-Exception while guessing content type", e);
+		}
+		return Optional.empty();
+	}
+
+	String getByMimeTypes(AttachmentFile attachmentFile) {
+		return mimetypesFileTypeMap.getContentType(attachmentFile.getName());
+	}
+
+	InputStream toInputStream(byte[] content) {
+		return new ByteArrayInputStream(content);
+	}
+}
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtService.java
index 813c1732414b2cfde6aee08b8bea55f1e4a49afe..d21051cb54f7afdd2aeda4422a5cd1c482e4cc76 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtService.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtService.java
@@ -27,6 +27,8 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Stream;
 
+import de.ozgcloud.nachrichten.file.AttachmentFile;
+
 //Temporally replacement for usign GRPC Api
 public interface PersistPostfachNachrichtService {
 
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtServiceImpl.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtServiceImpl.java
index 154404a4d8c7fb71f22e51016e004f8f63a0f5d6..cacd0ccc3fdfa334e5cdd7567484b7268961697f 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtServiceImpl.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/PersistPostfachNachrichtServiceImpl.java
@@ -42,6 +42,7 @@ import de.ozgcloud.apilib.vorgang.OzgCloudVorgangId;
 import de.ozgcloud.nachrichten.NachrichtenManagerConfiguration;
 import de.ozgcloud.nachrichten.attributes.ClientAttributeService;
 import de.ozgcloud.nachrichten.common.vorgang.VorgangService;
+import de.ozgcloud.nachrichten.file.AttachmentFile;
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht.Direction;
 
 @Service
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentService.java
index 0110fcc97ad21678c6c98d0871ff129feef6c52a..d2d27121106bfcebd58a5602ae4c4c37f13c0f89 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentService.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentService.java
@@ -24,45 +24,32 @@
 package de.ozgcloud.nachrichten.postfach.bayernid;
 
 import java.io.InputStream;
-import java.util.Optional;
 
-import org.bson.Document;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Service;
 
-import com.mongodb.client.gridfs.model.GridFSFile;
-
-import de.ozgcloud.common.errorhandling.TechnicalException;
-import de.ozgcloud.nachrichten.postfach.BinaryFileService;
+import de.ozgcloud.nachrichten.file.AttachmentFile;
+import de.ozgcloud.nachrichten.file.AttachmentFileService;
 import de.ozgcloud.nachrichten.postfach.FileId;
+import lombok.RequiredArgsConstructor;
 
 @Service
 @ConditionalOnProperty(prefix = "ozgcloud.bayernid", name = { "enabled" })
+@RequiredArgsConstructor
 public class BayernIdAttachmentService {
 
-	static final String NAME_KEY = "name";
-	static final String CONTENT_TYPE_KEY = "contentType";
-
-	@Autowired
-	private BinaryFileService binaryFileService;
+	private final AttachmentFileService attachmentFileService;
 
 	public BayernIdAttachment getMessageAttachment(FileId fileId) {
-		return Optional.ofNullable(binaryFileService.getFile(fileId))
-				.map(GridFSFile::getMetadata)
-				.map(metadata -> buildBayernIdAttachment(metadata, getAttachmentContentStream(fileId)))
-				.orElseThrow(() -> new TechnicalException("Can not find attachment with id " + fileId));
+		return buildBayernIdAttachment(attachmentFileService.getFile(fileId), attachmentFileService.getFileContent(fileId));
 	}
 
-	BayernIdAttachment buildBayernIdAttachment(Document metadata, InputStream attachmentContent) {
+	BayernIdAttachment buildBayernIdAttachment(AttachmentFile attachmentFile, InputStream attachmentContent) {
 		return BayernIdAttachment.builder()
-				.fileName(metadata.getString(NAME_KEY))
-				.contentType(metadata.getString(CONTENT_TYPE_KEY))
+				.fileName(attachmentFile.getName())
+				.contentType(attachmentFile.getContentType())
 				.content(attachmentContent)
 				.build();
 	}
 
-	InputStream getAttachmentContentStream(FileId fileId) {
-		return binaryFileService.getUploadedFileStream(fileId);
-	}
 }
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentService.java
index c6c2170130d3064b96fcd183ac141eac0f93e890..47dd7b5c7815121bd4b762ed0ae776dd0bd1967e 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentService.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentService.java
@@ -23,66 +23,60 @@
  */
 package de.ozgcloud.nachrichten.postfach.osi;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.Charset;
 import java.util.Base64;
 
 import org.apache.commons.io.IOUtils;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import de.ozgcloud.common.errorhandling.TechnicalException;
-import de.ozgcloud.nachrichten.postfach.AttachmentFile;
-import de.ozgcloud.nachrichten.postfach.BinaryFileService;
+import de.ozgcloud.nachrichten.file.AttachmentFile;
+import de.ozgcloud.nachrichten.file.AttachmentFileService;
 import de.ozgcloud.nachrichten.postfach.FileId;
-import de.ozgcloud.nachrichten.postfach.PersistPostfachNachrichtService;
-import lombok.val;
+import lombok.RequiredArgsConstructor;
 
 @Service
+@RequiredArgsConstructor
 public class MessageAttachmentService {
 
-	@Autowired
-	private PersistPostfachNachrichtService persistPostfachNachrichtService;
-
-	@Autowired
-	private BinaryFileService binaryFileService;
+	private final AttachmentFileService attachmentFileService;
 
 	public MessageAttachment getMessageAttachment(FileId fileId) {
+		return buildMessageAttachment(attachmentFileService.getFile(fileId), getAttachmentContent(fileId));
+	}
+
+	MessageAttachment buildMessageAttachment(AttachmentFile attachmentFile, String attachmentContent) {
+		return MessageAttachment.builder()
+				.fileName(attachmentFile.getName())
+				.content(attachmentContent)
+				.build();
+	}
+
+	String getAttachmentContent(FileId fileId) {
+		return encodeAttachmentContent(getContent(fileId));
+	}
+
+	byte[] getContent(FileId fileId) {
 		try {
-			val metadata = binaryFileService.getFile(fileId).getMetadata();
-			return MessageAttachment.builder()
-					.fileName(metadata.getString("name"))
-					.content(getAttachmentContent(fileId))
-					.build();
+			return IOUtils.toByteArray(attachmentFileService.getFileContent(fileId));
 		} catch (IOException e) {
-			throw new TechnicalException(e.getMessage(), e);
+			throw new TechnicalException("Cannot get file content", e);
 		}
 	}
 
-	String getAttachmentContent(FileId fileId) throws IOException {
-		ByteArrayOutputStream contentStream = new ByteArrayOutputStream();
-		IOUtils.copy(getAttachmentContentStream(fileId), contentStream);
-		return encodeAttachmentContent(contentStream.toByteArray());
-	}
-
-	private String encodeAttachmentContent(byte[] content) {
+	String encodeAttachmentContent(byte[] content) {
 		return new String(Base64.getEncoder().encode(content));
 	}
 
 	public String persistAttachment(String vorgangId, MessageAttachment attachment) {
-		return persistPostfachNachrichtService.persistAttachment(vorgangId, mapAttachmentFile(attachment));
+		return attachmentFileService.uploadAttachmentFile(buildAttachmentFile(vorgangId, attachment), attachment.getContent());
 	}
 
-	AttachmentFile mapAttachmentFile(MessageAttachment attachment) {
+	AttachmentFile buildAttachmentFile(String vorgangId, MessageAttachment attachment) {
 		return AttachmentFile.builder()
 				.name(attachment.getFileName())
-				.content(() -> IOUtils.toInputStream(attachment.getContent(), Charset.defaultCharset())).build();
-	}
-
-	InputStream getAttachmentContentStream(FileId fileId) {
-		return binaryFileService.getUploadedFileStream(fileId);
+				.vorgangId(vorgangId)
+				.build();
 	}
 
 }
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/NachrichtenManagerCallContextProviderTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/NachrichtenManagerCallContextProviderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ac62d8ed944b5c1565472216f3ccccc909f6a5d
--- /dev/null
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/NachrichtenManagerCallContextProviderTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+
+package de.ozgcloud.nachrichten;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import de.ozgcloud.apilib.user.OzgCloudUserId;
+import de.ozgcloud.apilib.vorgang.OzgCloudUserIdMapper;
+import de.ozgcloud.nachrichten.common.grpc.NachrichtenCallContextAttachingInterceptor;
+
+class NachrichtenManagerCallContextProviderTest {
+
+	@InjectMocks
+	private NachrichtenManagerCallContextProvider callContextProvider;
+
+	@Mock
+	private OzgCloudUserIdMapper userIdMapper;
+
+	@Nested
+	class TestProvideContext {
+
+		@BeforeEach
+		void init() {
+			when(userIdMapper.toUserId(anyString())).thenReturn(
+					OzgCloudUserId.from(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_SENDER_USER_ID));
+		}
+
+		@Test
+		void shouldSetClientName() {
+			var result = callContextProvider.provideContext();
+
+			assertThat(result.getClientName()).isEqualTo(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_CLIENT_NAME);
+		}
+
+		@Test
+		void shouldCallUserIdMapper() {
+			callContextProvider.provideContext();
+
+			verify(userIdMapper).toUserId(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_SENDER_USER_ID);
+		}
+
+		@Test
+		void shouldSetUserId() {
+			var result = callContextProvider.provideContext();
+
+			assertThat(result.getUserId()).hasToString(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_SENDER_USER_ID);
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/NachrichtenManagerTestApplication.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/NachrichtenManagerTestApplication.java
index 489b714cbb2abbcc18c4167fd1446781565734ce..6403b80171ca2e8f10953186048b44b90ca20ebb 100644
--- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/NachrichtenManagerTestApplication.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/NachrichtenManagerTestApplication.java
@@ -1,13 +1,16 @@
 package de.ozgcloud.nachrichten;
 
+import org.mapstruct.factory.Mappers;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 
 import de.ozgcloud.apilib.common.command.OzgCloudCommandService;
 import de.ozgcloud.apilib.file.OzgCloudFileService;
-import de.ozgcloud.nachrichten.postfach.BinaryFileService;
+import de.ozgcloud.apilib.vorgang.OzgCloudFileIdMapper;
+import de.ozgcloud.apilib.vorgang.OzgCloudUserIdMapper;
 import de.ozgcloud.nachrichten.postfach.muk.MukPostfachConfiguration;
 
 @SpringBootApplication
@@ -22,7 +25,15 @@ public class NachrichtenManagerTestApplication {
 	@MockBean
 	@Qualifier(MukPostfachConfiguration.OZG_CLOUD_FILE_SERVICE_NAME)
 	private OzgCloudFileService mukOzgCloudFileServices;
-	@MockBean
-	private BinaryFileService binaryFileService;
 
+
+	@Bean
+	OzgCloudFileIdMapper ozgCloudFileIdMapper() {
+		return Mappers.getMapper(OzgCloudFileIdMapper.class);
+	}
+
+	@Bean
+	OzgCloudUserIdMapper ozgCloudUserIdMapper() {
+		return Mappers.getMapper(OzgCloudUserIdMapper.class);
+	}
 }
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/common/grpc/NachrichtenCallContextAttachingInterceptorTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/common/grpc/NachrichtenCallContextAttachingInterceptorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8deb34be0350b52067847f983e13063f20745cbb
--- /dev/null
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/common/grpc/NachrichtenCallContextAttachingInterceptorTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+
+package de.ozgcloud.nachrichten.common.grpc;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import de.ozgcloud.common.grpc.GrpcUtil;
+import io.grpc.ClientCall;
+import io.grpc.Metadata;
+
+class NachrichtenCallContextAttachingInterceptorTest {
+
+	@InjectMocks
+	private NachrichtenCallContextAttachingInterceptor interceptor;
+
+	@Mock
+	private ClientCall<RequestTest, ResponseTest> delegateCall;
+	@Mock
+	private ClientCall.Listener<ResponseTest> responseTestListener;
+	@Mock
+	private Metadata requestMetadata;
+
+	private NachrichtenCallContextAttachingInterceptor.CallContextAttachingClientCall<RequestTest, ResponseTest> clientCall;
+
+	@BeforeEach
+	void init() {
+		clientCall = spy(interceptor.new CallContextAttachingClientCall<>(delegateCall));
+	}
+
+	@Nested
+	class TestStart {
+
+		@Mock
+		private Metadata customMetadata;
+
+		@BeforeEach
+		void init() {
+			doReturn(customMetadata).when(clientCall).buildCallContextMetadata();
+		}
+
+		@Test
+		void shouldCallBuildCallContextMetadata() {
+			clientCall.start(responseTestListener, requestMetadata);
+
+			verify(clientCall).buildCallContextMetadata();
+		}
+
+		@Test
+		void shouldMergeMetadata() {
+			clientCall.start(responseTestListener, requestMetadata);
+
+			verify(requestMetadata).merge(customMetadata);
+		}
+	}
+
+	@Nested
+	class TestBuildCallContextMetadata {
+
+		@Test
+		void shouldSetUserId() {
+			var result = clientCall.buildCallContextMetadata();
+
+			assertThat(result.get(GrpcUtil.HEADER_KEY_USER_ID))
+					.isEqualTo(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_SENDER_USER_ID.getBytes());
+		}
+
+		@Test
+		void shouldSetClientName() {
+			var result = clientCall.buildCallContextMetadata();
+
+			assertThat(result.get(GrpcUtil.HEADER_KEY_CLIENT_NAME))
+					.isEqualTo(NachrichtenCallContextAttachingInterceptor.NACHRICHTEN_MANAGER_CLIENT_NAME.getBytes());
+		}
+
+		@Test
+		void shouldCallGetrequestId() {
+			clientCall.buildCallContextMetadata();
+
+			verify(clientCall).getRequestId();
+		}
+
+		@Test
+		void shouldSetRequestId() {
+			var requestId = UUID.randomUUID().toString().getBytes();
+			doReturn(requestId).when(clientCall).getRequestId();
+
+			var result = clientCall.buildCallContextMetadata();
+
+			assertThat(result.get(GrpcUtil.HEADER_KEY_REQUEST_ID)).isEqualTo(requestId);
+		}
+	}
+
+	@Nested
+	class TestGetRequestId {
+
+		private MockedStatic<ThreadContext> threadContextMocked;
+
+		@BeforeEach
+		void init() {
+			threadContextMocked = Mockito.mockStatic(ThreadContext.class);
+		}
+
+		@AfterEach
+		void close() {
+			threadContextMocked.close();
+		}
+
+		@Test
+		void shouldCallGetContext() {
+			clientCall.getRequestId();
+
+			threadContextMocked.verify(ThreadContext::getContext);
+		}
+
+		@Test
+		void shouldReturnRequestIdFromThreadContext() {
+			var requestId = UUID.randomUUID().toString();
+
+			threadContextMocked.when(ThreadContext::getContext)
+					.thenReturn(Map.of(NachrichtenCallContextAttachingInterceptor.REQUEST_ID_KEY, requestId));
+
+			var result = clientCall.getRequestId();
+
+			assertThat(result).isEqualTo(requestId.getBytes());
+		}
+
+		@Test
+		void shouldReturnNewRequestId() {
+			var result = clientCall.getRequestId();
+
+			assertThat(result).isNotNull();
+		}
+	}
+
+	private record RequestTest() {
+	}
+
+	private record ResponseTest() {
+	}
+}
\ No newline at end of file
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileMapperTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileMapperTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..68dd898d3e247acbbb0176ca1debf15eacf9ca4e
--- /dev/null
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileMapperTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.nachrichten.file;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+
+import de.ozgcloud.apilib.file.OzgCloudFile;
+import de.ozgcloud.apilib.file.OzgCloudFileTestFactory;
+import de.ozgcloud.apilib.file.OzgCloudUploadFile;
+import de.ozgcloud.apilib.file.OzgCloudUploadFileTestFactory;
+
+class AttachmentFileMapperTest {
+
+	private final AttachmentFileMapper mapper = Mappers.getMapper(AttachmentFileMapper.class);
+
+	@Nested
+	class TestFromOzgCloudFile {
+
+		private static final OzgCloudFile OZG_CLOUD_FILE = OzgCloudFileTestFactory.createBuilder()
+				.contentType(AttachmentFileTestFactory.CONTENT_TYPE)
+				.name(AttachmentFileTestFactory.NAME)
+				.build();
+
+		@Test
+		void shouldMapFromOzgCloudFile() {
+
+			var result = mapper.fromOzgCloudFile(OZG_CLOUD_FILE);
+
+			assertThat(result).usingRecursiveComparison().ignoringFields("vorgangId").isEqualTo(AttachmentFileTestFactory.create());
+		}
+	}
+
+	@Nested
+	class TestToOzgCloudUploadFile {
+
+		private static final OzgCloudUploadFile OZG_CLOUD_UPLOAD_FILE = OzgCloudUploadFileTestFactory.createBuilder()
+				.fieldName(AttachmentFileService.ATTACHMENT_NAME)
+				.fileName(AttachmentFileTestFactory.NAME)
+				.contentType(AttachmentFileTestFactory.CONTENT_TYPE)
+				.vorgangId(AttachmentFileTestFactory.VORGANG_ID)
+				.build();
+
+		@Test
+		void shouldMapToOzgCloudUploadFile() {
+			var result = mapper.toOzgCloudUploadFile(AttachmentFileTestFactory.create());
+
+			assertThat(result).usingRecursiveComparison().isEqualTo(OZG_CLOUD_UPLOAD_FILE);
+		}
+	}
+}
\ No newline at end of file
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileServiceTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5936b2e23f732afcd1ad73e2b23722d04c09016
--- /dev/null
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileServiceTest.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.nachrichten.file;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.net.FileNameMap;
+import java.net.URLConnection;
+import java.util.Base64;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import jakarta.activation.MimetypesFileTypeMap;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Spy;
+import org.springframework.core.task.TaskExecutor;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.apilib.file.OzgCloudFile;
+import de.ozgcloud.apilib.file.OzgCloudFileId;
+import de.ozgcloud.apilib.file.OzgCloudFileService;
+import de.ozgcloud.apilib.file.OzgCloudUploadFile;
+import de.ozgcloud.apilib.vorgang.OzgCloudFileIdMapper;
+import de.ozgcloud.common.test.TestUtils;
+import de.ozgcloud.nachrichten.postfach.FileId;
+import lombok.SneakyThrows;
+
+class AttachmentFileServiceTest {
+
+	private static final String FILE_ID = UUID.randomUUID().toString();
+	private static final byte[] FILE_CONTENT = LoremIpsum.getInstance().getWords(1).getBytes();
+
+	@Spy
+	@InjectMocks
+	private AttachmentFileService service;
+
+	@Mock
+	private OzgCloudFileService ozgCloudFileService;
+	@Mock
+	private OzgCloudFileIdMapper fileIdMapper;
+	@Mock
+	private AttachmentFileMapper attachmentFileMapper;
+	@Mock
+	private TaskExecutor taskExecutor;
+	@Mock
+	private MimetypesFileTypeMap mimetypesFileTypeMap;
+
+	@Nested
+	class TestGetFile {
+
+		@Mock
+		private OzgCloudFile ozgCloudFile;
+		@Mock
+		private AttachmentFile attachmentFile;
+
+		@BeforeEach
+		void init() {
+			when(fileIdMapper.toFileId(any())).thenReturn(OzgCloudFileId.from(FILE_ID));
+			when(attachmentFileMapper.fromOzgCloudFile(any())).thenReturn(attachmentFile);
+			when(ozgCloudFileService.getFile(any())).thenReturn(ozgCloudFile);
+		}
+
+		@Test
+		void shouldCallToFileId() {
+			getFile();
+
+			verify(fileIdMapper).toFileId(FILE_ID);
+		}
+
+		@Test
+		void shouldCallGetFile() {
+			getFile();
+
+			verify(ozgCloudFileService).getFile(OzgCloudFileId.from(FILE_ID));
+		}
+
+		@Test
+		void shouldCallFromOzgCloudFile() {
+			getFile();
+
+			verify(attachmentFileMapper).fromOzgCloudFile(ozgCloudFile);
+		}
+
+		@Test
+		void shouldReturnAttachmentFile() {
+			var result = getFile();
+
+			assertThat(result).isEqualTo(attachmentFile);
+		}
+
+		private AttachmentFile getFile() {
+			return service.getFile(FileId.from(FILE_ID));
+		}
+	}
+
+	@Nested
+	class TestGetFileContent {
+
+		@Mock
+		private PipedInputStream inputStream;
+		@Mock
+		private OutputStream outputStream;
+
+		@BeforeEach
+		void init() {
+			doReturn(inputStream).when(service).createInputStream();
+			doReturn(outputStream).when(service).connectToOutputStream(any());
+			doNothing().when(service).writeFileContent(any(), any());
+		}
+
+		@Test
+		void shouldCallCreateInputStream() {
+			getFileContent();
+
+			verify(service).createInputStream();
+		}
+
+		@Test
+		void shouldCallConnectToOutputStream() {
+			getFileContent();
+
+			verify(service).connectToOutputStream(inputStream);
+		}
+
+		@Test
+		void shoulcCallWriteFileContent() {
+			getFileContent();
+
+			verify(service).writeFileContent(FILE_ID, outputStream);
+		}
+
+		@Test
+		void shouldReturnContentStream() {
+			var result = getFileContent();
+
+			assertThat(result).isEqualTo(inputStream);
+		}
+
+		private InputStream getFileContent() {
+			return service.getFileContent(FileId.from(FILE_ID));
+		}
+	}
+
+	@Nested
+	class TestConnectToOutputStream {
+
+		@SneakyThrows
+		@Test
+		void shouldReturnOutputStream() {
+			try (var inputStream = new PipedInputStream()) {
+
+				var result = service.connectToOutputStream(inputStream);
+				result.write(FILE_CONTENT);
+				result.close();
+
+				assertThat(inputStream).hasBinaryContent(FILE_CONTENT);
+			}
+		}
+	}
+
+	@Nested
+	class TestWriteFileContent {
+
+		private static final OzgCloudFileId OZG_CLOUD_FILE_ID = OzgCloudFileId.from(FILE_ID);
+
+		@Mock
+		private OutputStream outputStream;
+
+		@Captor
+		private ArgumentCaptor<Runnable> runnableCaptor;
+
+		@BeforeEach
+		void init() {
+			when(fileIdMapper.toFileId(any())).thenReturn(OZG_CLOUD_FILE_ID);
+		}
+
+		@Test
+		void shouldCallToFileId() {
+			writeFileContent();
+
+			verify(fileIdMapper).toFileId(FILE_ID);
+		}
+
+		@Test
+		void shouldCallWriteFileDataToStream() {
+			writeFileContent();
+
+			verify(ozgCloudFileService).writeFileDataToStream(OZG_CLOUD_FILE_ID, outputStream);
+		}
+
+		@Test
+		void shouldCallCloseOutputStream() {
+			try (var ioUtilsMock = mockStatic(IOUtils.class)) {
+				writeFileContent();
+
+				ioUtilsMock.verify(() -> IOUtils.closeQuietly(eq(outputStream), any(Consumer.class)));
+			}
+		}
+
+		private void writeFileContent() {
+			service.writeFileContent(FILE_ID, outputStream);
+			verify(taskExecutor).execute(runnableCaptor.capture());
+			runnableCaptor.getValue().run();
+		}
+
+	}
+
+	@Nested
+	class TestUploadAttachmentFile {
+
+		private static final AttachmentFile ATTACHMENT_FILE = AttachmentFileTestFactory.create();
+		private static final String FILE_CONTENT_STR = new String(FILE_CONTENT);
+
+		@Mock
+		private AttachmentFile enhancedAttachmentFile;
+		@Mock
+		private OzgCloudUploadFile ozgCloudUploadFile;
+		@Mock
+		private InputStream contentStream;
+		@Mock
+		private Base64.Decoder decoder;
+
+		private MockedStatic<Base64> base64Mock;
+
+		@BeforeEach
+		void init() {
+			when(ozgCloudFileService.uploadFile(any(), any())).thenReturn(OzgCloudFileId.from(FILE_ID));
+			when(attachmentFileMapper.toOzgCloudUploadFile(any())).thenReturn(ozgCloudUploadFile);
+			doReturn(enhancedAttachmentFile).when(service).addContentTypeIfMissing(any(), any());
+			doReturn(contentStream).when(service).toInputStream(any());
+			base64Mock = mockStatic(Base64.class);
+			base64Mock.when(Base64::getDecoder).thenReturn(decoder);
+			when(decoder.decode(anyString())).thenReturn(FILE_CONTENT);
+		}
+
+		@AfterEach
+		void close() {
+			base64Mock.close();
+		}
+
+		@Test
+		void shouldCallGetDecoder() {
+			uploadAttachmentFile();
+
+			base64Mock.verify(Base64::getDecoder);
+		}
+
+		@Test
+		void shouldCallDecode() {
+			uploadAttachmentFile();
+
+			verify(decoder).decode(FILE_CONTENT_STR);
+		}
+
+		@Test
+		void shouldCallAddContentTypeIfMissing() {
+			uploadAttachmentFile();
+
+			verify(service).addContentTypeIfMissing(ATTACHMENT_FILE, FILE_CONTENT);
+		}
+
+		@Test
+		void shouldCallToOzgCloudUploadFile() {
+			uploadAttachmentFile();
+
+			verify(attachmentFileMapper).toOzgCloudUploadFile(enhancedAttachmentFile);
+		}
+
+		@Test
+		void shouldCallToDecodedInputStream() {
+			uploadAttachmentFile();
+
+			verify(service).toInputStream(FILE_CONTENT);
+		}
+
+		@Test
+		void shouldCallUploadFile() {
+			uploadAttachmentFile();
+
+			verify(ozgCloudFileService).uploadFile(ozgCloudUploadFile, contentStream);
+		}
+
+		@Test
+		void shouldReturnFileId() {
+			var result = uploadAttachmentFile();
+
+			assertThat(result).isEqualTo(FILE_ID);
+		}
+
+		private String uploadAttachmentFile() {
+			return service.uploadAttachmentFile(ATTACHMENT_FILE, FILE_CONTENT_STR);
+		}
+	}
+
+	@Nested
+	class TestAddContentTypeIfMissing {
+
+		private AttachmentFile attachmentFile;
+
+		@Nested
+		class TestNoContentType {
+
+			@BeforeEach
+			void init() {
+				attachmentFile = AttachmentFileTestFactory.createBuilder().contentType(null).build();
+				doReturn(AttachmentFileTestFactory.CONTENT_TYPE).when(service).getContentType(any(), any());
+			}
+
+			@Test
+			void shouldCallGetContentType() {
+				addContentTypeIfMissing();
+
+				verify(service).getContentType(attachmentFile, FILE_CONTENT);
+			}
+
+			@Test
+			void shouldReturnAttachmentFile() {
+				var result = addContentTypeIfMissing();
+
+				assertThat(result).usingRecursiveComparison().isEqualTo(AttachmentFileTestFactory.create());
+			}
+		}
+
+		@Test
+		void shouldReturnAttachmentFile() {
+			attachmentFile = AttachmentFileTestFactory.create();
+
+			var result = addContentTypeIfMissing();
+
+			assertThat(result).isSameAs(attachmentFile);
+		}
+
+		private AttachmentFile addContentTypeIfMissing() {
+			return service.addContentTypeIfMissing(attachmentFile, FILE_CONTENT);
+		}
+	}
+
+	@Nested
+	class TestGetContentType {
+
+		private static final AttachmentFile ATTACHMENT_FILE = AttachmentFileTestFactory.create();
+
+		@Nested
+		class TestGetByFileName {
+
+			@BeforeEach
+			void init() {
+				doReturn(Optional.of(AttachmentFileTestFactory.CONTENT_TYPE)).when(service).getTypeByFileName(any());
+			}
+
+			@Test
+			void shouldCallGetTypeByFileName() {
+				getContentType();
+
+				verify(service).getTypeByFileName(ATTACHMENT_FILE);
+			}
+
+			@Test
+			void shouldReturnContentType() {
+				var result = getContentType();
+
+				assertThat(result).isEqualTo(AttachmentFileTestFactory.CONTENT_TYPE);
+			}
+		}
+
+		@Nested
+		class TestTypeByContent {
+
+			@BeforeEach
+			void init() {
+				doReturn(Optional.empty()).when(service).getTypeByFileName(any());
+				doReturn(Optional.of(AttachmentFileTestFactory.CONTENT_TYPE)).when(service).getTypeByContent(any());
+			}
+
+			@Test
+			void shouldCallGetTypeByContent() {
+				getContentType();
+
+				verify(service).getTypeByContent(FILE_CONTENT);
+			}
+
+			@Test
+			void shouldReturnContentType() {
+				var result = getContentType();
+
+				assertThat(result).isEqualTo(AttachmentFileTestFactory.CONTENT_TYPE);
+			}
+
+		}
+
+		@Nested
+		class TestByMimeTypes {
+
+			@BeforeEach
+			void init() {
+				doReturn(Optional.empty()).when(service).getTypeByFileName(any());
+				doReturn(Optional.empty()).when(service).getTypeByContent(any());
+				doReturn(AttachmentFileTestFactory.CONTENT_TYPE).when(service).getByMimeTypes(any());
+			}
+
+			@Test
+			void shouldCallGetByMimeTypes() {
+				getContentType();
+
+				verify(service).getByMimeTypes(ATTACHMENT_FILE);
+			}
+
+			@Test
+			void shouldReturnContentType() {
+				var result = getContentType();
+
+				assertThat(result).isEqualTo(AttachmentFileTestFactory.CONTENT_TYPE);
+			}
+		}
+
+		private String getContentType() {
+			return service.getContentType(ATTACHMENT_FILE, FILE_CONTENT);
+		}
+	}
+
+	@Nested
+	class TestGetTypeByFileName {
+
+		private static final AttachmentFile ATTACHMENT_FILE = AttachmentFileTestFactory.create();
+
+		@Mock
+		private FileNameMap fileNameMap;
+		private MockedStatic<URLConnection> urlConnectionMock;
+
+		@BeforeEach
+		void init() {
+			urlConnectionMock = mockStatic(URLConnection.class);
+			urlConnectionMock.when(URLConnection::getFileNameMap).thenReturn(fileNameMap);
+		}
+
+		@AfterEach
+		void close() {
+			urlConnectionMock.close();
+		}
+
+		@Test
+		void shouldCallGetContentTypeFor() {
+			getTypeByFileName();
+
+			verify(fileNameMap).getContentTypeFor(AttachmentFileTestFactory.NAME);
+		}
+
+		@Test
+		void shouldReturnContentType() {
+			when(fileNameMap.getContentTypeFor(any())).thenReturn(AttachmentFileTestFactory.CONTENT_TYPE);
+
+			var result = getTypeByFileName();
+
+			assertThat(result).contains(AttachmentFileTestFactory.CONTENT_TYPE);
+		}
+
+		@Test
+		void shouldReturnEmpty() {
+			var result = getTypeByFileName();
+
+			assertThat(result).isEmpty();
+		}
+
+		private Optional<String> getTypeByFileName() {
+			return service.getTypeByFileName(ATTACHMENT_FILE);
+		}
+	}
+
+	@Nested
+	class TestGetTypeByContent {
+
+		@SneakyThrows
+		@Test
+		void shouldReturnContentType() {
+			var content = TestUtils.loadFile("BspQuittung.xml").readAllBytes();
+
+			var result = service.getTypeByContent(content);
+
+			assertThat(result).contains("application/xml");
+		}
+
+		@Test
+		void shouldReturnEmpty() {
+			var result = service.getTypeByContent(FILE_CONTENT);
+
+			assertThat(result).isEmpty();
+		}
+
+		@Test
+		void shouldNotThrowException() {
+			try (var urlConnectionMock = mockStatic(URLConnection.class)) {
+				urlConnectionMock.when(() -> URLConnection.guessContentTypeFromStream(any())).thenThrow(IOException.class);
+
+				var result = service.getTypeByContent(FILE_CONTENT);
+
+				assertThat(result).isEmpty();
+			}
+
+		}
+
+		@Nested
+		class TestGetByMimeTypes {
+
+			@Test
+			void shouldCallGetContentType() {
+				getByMimeTypes();
+
+				verify(mimetypesFileTypeMap).getContentType(AttachmentFileTestFactory.NAME);
+			}
+
+			@Test
+			void shouldReturnContentType() {
+				when(mimetypesFileTypeMap.getContentType(anyString())).thenReturn(AttachmentFileTestFactory.CONTENT_TYPE);
+
+				var result = getByMimeTypes();
+
+				assertThat(result).isEqualTo(AttachmentFileTestFactory.CONTENT_TYPE);
+			}
+
+			private String getByMimeTypes() {
+				return service.getByMimeTypes(AttachmentFileTestFactory.create());
+			}
+		}
+	}
+
+	@Nested
+	class TestToInputStream {
+
+		@Test
+		void shouldReturnInputStream() {
+			var result = service.toInputStream(FILE_CONTENT);
+
+			assertThat(result).hasBinaryContent(FILE_CONTENT);
+		}
+	}
+}
\ No newline at end of file
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/AttachmentFile.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileTestFactory.java
similarity index 55%
rename from nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/AttachmentFile.java
rename to nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileTestFactory.java
index 0133636869be97afda3d6b5f1475c5773fe5a720..fc2a3ac29ea6a095b8108e882e0ad028293e9b14 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/postfach/AttachmentFile.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/file/AttachmentFileTestFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
  * Ministerpräsidenten des Landes Schleswig-Holstein
  * Staatskanzlei
  * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
@@ -21,33 +21,26 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-package de.ozgcloud.nachrichten.postfach;
+package de.ozgcloud.nachrichten.file;
 
-import static java.util.Objects.*;
+import com.thedeanda.lorem.LoremIpsum;
 
-import java.io.InputStream;
-import java.util.function.Supplier;
+import de.ozgcloud.nachrichten.common.vorgang.VorgangTestFactory;
 
-import lombok.AccessLevel;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.ToString;
+public class AttachmentFileTestFactory {
 
-@Builder
-@Getter
-@ToString
-public class AttachmentFile {
-	private String name;
-	private String contentType;
-	@Getter(AccessLevel.NONE)
-	@ToString.Exclude
-	private Supplier<InputStream> content;
+	public static final String NAME = LoremIpsum.getInstance().getWords(1);
+	public static final String CONTENT_TYPE = LoremIpsum.getInstance().getWords(1);
+	public static final String VORGANG_ID = VorgangTestFactory.ID;
 
-	public InputStream getContent() {
-		if (isNull(content)) {
-			return InputStream.nullInputStream();
-		}
-		return content.get();
+	public static AttachmentFile create() {
+		return createBuilder().build();
 	}
 
+	public static AttachmentFile.AttachmentFileBuilder createBuilder() {
+		return AttachmentFile.builder()
+				.name(NAME)
+				.contentType(CONTENT_TYPE)
+				.vorgangId(VORGANG_ID);
+	}
 }
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentServiceTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentServiceTest.java
index 5db43d5340835019620df44b1c52465a50dd793d..67be8d6b18244c0926b10e1f7d3bc07955b3dddc 100644
--- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentServiceTest.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentServiceTest.java
@@ -25,26 +25,22 @@ package de.ozgcloud.nachrichten.postfach.bayernid;
 
 import static de.ozgcloud.nachrichten.postfach.bayernid.BayernIdAttachmentTestFactory.*;
 import static org.assertj.core.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 
-import org.bson.Document;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
 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 com.mongodb.client.gridfs.model.GridFSFile;
-
-import de.ozgcloud.common.errorhandling.TechnicalException;
-import de.ozgcloud.nachrichten.postfach.BinaryFileService;
+import de.ozgcloud.nachrichten.file.AttachmentFile;
+import de.ozgcloud.nachrichten.file.AttachmentFileService;
+import de.ozgcloud.nachrichten.file.AttachmentFileTestFactory;
 import de.ozgcloud.nachrichten.postfach.FileId;
 
 class BayernIdAttachmentServiceTest {
@@ -53,74 +49,54 @@ class BayernIdAttachmentServiceTest {
 
 	@InjectMocks
 	@Spy
-	private BayernIdAttachmentService bayernIdAttachmentService;
-
-	@Mock
-	private BinaryFileService fileService;
-
-	@Mock
-	private GridFSFile gridFsfile;
+	private BayernIdAttachmentService service;
 
 	@Mock
-	private Document metadata;
+	private AttachmentFileService attachmentFileService;
 
 	@Nested
-	class TestLoadingAttachment {
+	class TestGetMessageAttachment {
+
+		@Mock
+		private AttachmentFile attachmentFile;
+		@Mock
+		private InputStream fileContent;
+		@Mock
+		private BayernIdAttachment bayernIdAttachment;
 
 		@BeforeEach
 		void init() {
-			when(gridFsfile.getMetadata()).thenReturn(metadata);
-			when(fileService.getFile(any())).thenReturn(gridFsfile);
+			when(attachmentFileService.getFile(any())).thenReturn(attachmentFile);
+			when(attachmentFileService.getFileContent(any())).thenReturn(fileContent);
+			doReturn(bayernIdAttachment).when(service).buildBayernIdAttachment(any(), any());
 		}
 
 		@Test
 		void shouldCallGetFile() {
-			when(fileService.getUploadedFileStream(any())).thenReturn(new ByteArrayInputStream(CONTENT));
+			service.getMessageAttachment(FILE_ID);
 
-			bayernIdAttachmentService.getMessageAttachment(FILE_ID);
-
-			verify(fileService).getFile(FILE_ID);
+			verify(attachmentFileService).getFile(FILE_ID);
 		}
 
 		@Test
-		void shouldCallGetAttachmentContent() {
-			when(fileService.getUploadedFileStream(any())).thenReturn(new ByteArrayInputStream(CONTENT));
-
-			bayernIdAttachmentService.getMessageAttachment(FILE_ID);
+		void shouldCallGetFileContent() {
+			service.getMessageAttachment(FILE_ID);
 
-			verify(bayernIdAttachmentService).getMessageAttachment(FILE_ID);
+			verify(attachmentFileService).getFileContent(FILE_ID);
 		}
 
 		@Test
 		void shouldCallBuildBayernIdAttachment() {
-			var contentStream = getContentStream();
-			doReturn(contentStream).when(bayernIdAttachmentService).getAttachmentContentStream(any());
+			service.getMessageAttachment(FILE_ID);
 
-			bayernIdAttachmentService.getMessageAttachment(FILE_ID);
-
-			verify(bayernIdAttachmentService).buildBayernIdAttachment(metadata, contentStream);
+			verify(service).buildBayernIdAttachment(attachmentFile, fileContent);
 		}
 
-	}
-
-	@Nested
-	class TestLoadAttachmentError {
-
 		@Test
-		@DisplayName("should throw TechnicalException if attachment not found")
-		void shouldThrowException() {
-			when(fileService.getFile(any())).thenReturn(null);
+		void shouldReturnBayernIdAttachment() {
+			var attachment = service.getMessageAttachment(FILE_ID);
 
-			assertThrows(TechnicalException.class, () -> bayernIdAttachmentService.getMessageAttachment(FILE_ID));
-		}
-
-		@Test
-		@DisplayName("should throw TechnicalException if metadata is null")
-		void shouldThrowExceptionIfNoMetadata() {
-			when(fileService.getFile(any())).thenReturn(gridFsfile);
-			when(gridFsfile.getMetadata()).thenReturn(null);
-
-			assertThrows(TechnicalException.class, () -> bayernIdAttachmentService.getMessageAttachment(FILE_ID));
+			assertThat(attachment).isSameAs(bayernIdAttachment);
 		}
 	}
 
@@ -129,8 +105,6 @@ class BayernIdAttachmentServiceTest {
 
 		@BeforeEach
 		void setup() {
-			when(metadata.getString(BayernIdAttachmentService.NAME_KEY)).thenReturn(BayernIdAttachmentTestFactory.FILENAME);
-			when(metadata.getString(BayernIdAttachmentService.CONTENT_TYPE_KEY)).thenReturn(BayernIdAttachmentTestFactory.CONTENT_TYPE);
 		}
 
 		@Test
@@ -155,25 +129,7 @@ class BayernIdAttachmentServiceTest {
 		}
 
 		private BayernIdAttachment buildBayernIdAttachment() {
-			return bayernIdAttachmentService.buildBayernIdAttachment(metadata, getContentStream());
-		}
-	}
-
-	@Nested
-	class TestLoadingAttachmentContent {
-
-		@BeforeEach
-		void init() {
-			when(fileService.getUploadedFileStream(any()))
-					.thenReturn(new ByteArrayInputStream(CONTENT));
+			return service.buildBayernIdAttachment(AttachmentFileTestFactory.create(), getContentStream());
 		}
-
-		@Test
-		void shouldGetInputStream() {
-			InputStream input = bayernIdAttachmentService.getAttachmentContentStream(FILE_ID);
-
-			assertThat(input).hasSameContentAs(getContentStream());
-		}
-
 	}
 }
\ No newline at end of file
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentTestFactory.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentTestFactory.java
index 2fe959b6e55845a65ad5596239816a23945759b3..0ae8abb39ab76a2dc5548865a0ea84ccf61935a3 100644
--- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentTestFactory.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/bayernid/BayernIdAttachmentTestFactory.java
@@ -26,12 +26,13 @@ package de.ozgcloud.nachrichten.postfach.bayernid;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 
+import de.ozgcloud.nachrichten.file.AttachmentFileTestFactory;
 import de.ozgcloud.nachrichten.postfach.bayernid.BayernIdAttachment.BayernIdAttachmentBuilder;
 
 public class BayernIdAttachmentTestFactory {
 
-	public final static String FILENAME = "test.txt";
-	public final static String CONTENT_TYPE = "text/plain";
+	public final static String FILENAME = AttachmentFileTestFactory.NAME;
+	public final static String CONTENT_TYPE = AttachmentFileTestFactory.CONTENT_TYPE;
 	public static final byte[] CONTENT = "test".getBytes();
 	public static final long SIZE = 4L;
 
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentServiceTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentServiceTest.java
index dc473e132bf0dff050351527495b0c78b211ff09..3a16696cab0b269da392fc7fc372b6323d913890 100644
--- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentServiceTest.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentServiceTest.java
@@ -28,104 +28,217 @@ import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.io.InputStream;
-import java.sql.Date;
-import java.time.Instant;
 
-import org.bson.BsonObjectId;
-import org.bson.BsonValue;
-import org.bson.Document;
 import org.junit.jupiter.api.BeforeEach;
 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 com.mongodb.client.gridfs.model.GridFSFile;
+import com.thedeanda.lorem.LoremIpsum;
 
-import de.ozgcloud.nachrichten.postfach.BinaryFileService;
+import de.ozgcloud.nachrichten.file.AttachmentFile;
+import de.ozgcloud.nachrichten.file.AttachmentFileService;
+import de.ozgcloud.nachrichten.file.AttachmentFileTestFactory;
 import de.ozgcloud.nachrichten.postfach.FileId;
 
 class MessageAttachmentServiceTest {
-	private static final FileId FILE_ID = FileId.from("42");
 
+	private static final FileId FILE_ID = FileId.from(LoremIpsum.getInstance().getWords(1));
+
+	@Spy
 	@InjectMocks
-	private MessageAttachmentService messageAttachmentService;
+	private MessageAttachmentService service;
 
 	@Mock
-	private BinaryFileService fileService;
-
-	private GridFSFile gridFsfile;
+	private AttachmentFileService attachmentFileService;
 
 	@Nested
-	class TestLoadingAttachment {
+	class TestGetMessageAttachment {
+
+		@Mock
+		private AttachmentFile attachmentFile;
+		@Mock
+		private MessageAttachment messageAttachment;
+
 		@BeforeEach
 		void init() {
-			Document metadata = new Document();
-			metadata.put("name", MessageAttachmentTestFactory.FILENAME);
-			BsonValue id = new BsonObjectId();
-			gridFsfile = new GridFSFile(id, FILE_ID.toString(), 0, 0, Date.from(Instant.now()), metadata);
+			when(attachmentFileService.getFile(any())).thenReturn(attachmentFile);
+			doReturn(MessageAttachmentTestFactory.CONTENT).when(service).getAttachmentContent(any());
+			doReturn(messageAttachment).when(service).buildMessageAttachment(any(), any());
+		}
+
+		@Test
+		void shouldCallGetFile() {
+			getMessageAttachment();
+
+			verify(attachmentFileService).getFile(FILE_ID);
+		}
 
-			when(fileService.getFile(any())).thenReturn(gridFsfile);
-			when(fileService.getUploadedFileStream(any()))
-					.thenReturn(new ByteArrayInputStream(MessageAttachmentTestFactory.DECODED_CONTENT.getBytes()));
+		@Test
+		void shouldCallGetAttachmentContent() {
+			getMessageAttachment();
+
+			verify(service).getAttachmentContent(FILE_ID);
 		}
 
 		@Test
-		void shouldHaveAttachmentWithFileName() {
-			MessageAttachment attachment = messageAttachmentService.getMessageAttachment(FILE_ID);
+		void shouldCallBuildMessageAttachment() {
+			getMessageAttachment();
 
-			assertThat(attachment.getFileName()).isEqualTo(MessageAttachmentTestFactory.FILENAME);
+			verify(service).buildMessageAttachment(attachmentFile, MessageAttachmentTestFactory.CONTENT);
 		}
 
 		@Test
-		void shouldHaveAttachmentContent() {
-			MessageAttachment attachment = messageAttachmentService.getMessageAttachment(FILE_ID);
+		void shouldReturnMessageAttachment() {
+			var result = getMessageAttachment();
 
-			assertThat(attachment.getContent()).isEqualTo(MessageAttachmentTestFactory.CONTENT);
+			assertThat(result).isSameAs(messageAttachment);
 		}
 
+		private MessageAttachment getMessageAttachment() {
+			return service.getMessageAttachment(FILE_ID);
+		}
 	}
 
 	@Nested
-	class TestLoadingAttachmentContent {
+	class TestBuildMessageAttachment {
+
+		@Test
+		void shouldBuildMessageAttachment() {
+			var attachment = buildMessageAttachment();
+
+			assertThat(attachment).usingRecursiveComparison().isEqualTo(MessageAttachmentTestFactory.create());
+		}
+
+		private MessageAttachment buildMessageAttachment() {
+			return service.buildMessageAttachment(AttachmentFileTestFactory.create(), MessageAttachmentTestFactory.CONTENT);
+		}
+	}
+
+	@Nested
+	class TestGetAttachmentContent {
+
+		private static final byte[] CONTENT = MessageAttachmentTestFactory.CONTENT.getBytes();
+
 		@BeforeEach
 		void init() {
-			when(fileService.getUploadedFileStream(any()))
-					.thenReturn(new ByteArrayInputStream(MessageAttachmentTestFactory.DECODED_CONTENT.getBytes()));
+			doReturn(CONTENT).when(service).getContent(any(FileId.class));
+			doReturn(MessageAttachmentTestFactory.CONTENT).when(service).encodeAttachmentContent(any());
+		}
+
+		@Test
+		void shouldCallGetContent() {
+			service.getAttachmentContent(FILE_ID);
+
+			verify(service).getContent(FILE_ID);
 		}
 
 		@Test
-		void shouldGetInputStream() {
-			InputStream input = messageAttachmentService.getAttachmentContentStream(FILE_ID);
+		void shouldCallEncodeAttachmentContent() {
+			service.getAttachmentContent(FILE_ID);
 
-			assertThat(input).isNotNull();
+			verify(service).encodeAttachmentContent(CONTENT);
 		}
 
 		@Test
-		void shouldGetContent() throws IOException {
-			String input = messageAttachmentService.getAttachmentContent(FILE_ID);
+		void shouldReturnContent() {
+			var result = service.getAttachmentContent(FILE_ID);
 
-			assertThat(input).isEqualTo(MessageAttachmentTestFactory.CONTENT);
+			assertThat(result).isEqualTo(MessageAttachmentTestFactory.CONTENT);
 		}
 	}
 
 	@Nested
-	class TestMapAttachmentFile {
+	class TestGetContent {
 
 		@Test
-		void shouldMapFileName() {
-			var attachmentFile = messageAttachmentService.mapAttachmentFile(MessageAttachmentTestFactory.create());
+		void shouldCallGetFileContent() {
+			when(attachmentFileService.getFileContent(any())).thenReturn(new ByteArrayInputStream(MessageAttachmentTestFactory.CONTENT.getBytes()));
+
+			service.getContent(FILE_ID);
+
+			verify(attachmentFileService).getFileContent(FILE_ID);
+		}
+
+		@Test
+		void shouldReturnContent() {
+			when(attachmentFileService.getFileContent(any())).thenReturn(new ByteArrayInputStream(MessageAttachmentTestFactory.CONTENT.getBytes()));
+
+			var result = service.getContent(FILE_ID);
+
+			assertThat(result).isEqualTo(MessageAttachmentTestFactory.CONTENT.getBytes());
+		}
+	}
+
+	@Nested
+	class TestEncodeAttachmentContent {
+
+		@Test
+		void shouldEncodeContent() {
+			var result = service.encodeAttachmentContent(MessageAttachmentTestFactory.DECODED_CONTENT.getBytes());
+
+			assertThat(result).isEqualTo(MessageAttachmentTestFactory.CONTENT);
+		}
+	}
 
-			assertThat(attachmentFile.getName()).isEqualTo(MessageAttachmentTestFactory.FILENAME);
+	@Nested
+	class TestPersistAttachment {
+
+		private static final String ATTACHMENT_FILE_ID = LoremIpsum.getInstance().getWords(1);
+		private static final MessageAttachment ATTACHMENT = MessageAttachmentTestFactory.create();
+
+		@Mock
+		private AttachmentFile attachmentFile;
+		@Mock
+		private InputStream inputStream;
+
+		@BeforeEach
+		void init() {
+			doReturn(attachmentFile).when(service).buildAttachmentFile(any(), any());
+			when(attachmentFileService.uploadAttachmentFile(any(), any())).thenReturn(ATTACHMENT_FILE_ID);
 		}
 
 		@Test
-		void shouldMapContent() {
-			var attachmentFile = messageAttachmentService.mapAttachmentFile(MessageAttachmentTestFactory.create());
+		void shouldCallBuildAttachmentFile() {
+			persistAttachment();
+
+			verify(service).buildAttachmentFile(AttachmentFileTestFactory.VORGANG_ID, ATTACHMENT);
+		}
+
+		@Test
+		void shouldCallCreateAttachmentFile() {
+			persistAttachment();
+
+			verify(attachmentFileService).uploadAttachmentFile(attachmentFile, MessageAttachmentTestFactory.CONTENT);
+		}
+
+		@Test
+		void shouldReturnAttachmentFileId() {
+			var result = persistAttachment();
+
+			assertThat(result).isEqualTo(ATTACHMENT_FILE_ID);
+		}
+
+		private String persistAttachment() {
+			return service.persistAttachment(AttachmentFileTestFactory.VORGANG_ID, ATTACHMENT);
+		}
+	}
+
+	@Nested
+	class TestBuildAttachmentFile {
+
+		@Test
+		void shouldMapFileName() {
+			var attachmentFile = buildAttachmentFile();
+
+			assertThat(attachmentFile).usingRecursiveComparison().ignoringFields("contentType").isEqualTo(AttachmentFileTestFactory.create());
+		}
 
-			assertThat(attachmentFile.getContent()).hasContent(MessageAttachmentTestFactory.CONTENT);
+		private AttachmentFile buildAttachmentFile() {
+			return service.buildAttachmentFile(AttachmentFileTestFactory.VORGANG_ID, MessageAttachmentTestFactory.create());
 		}
 	}
 }
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentTestFactory.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentTestFactory.java
index 17181b240ad7645e49afd5c977cefdb925226425..b0ca53c550055aa6c20865dec1745589dd2bb865 100644
--- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentTestFactory.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/postfach/osi/MessageAttachmentTestFactory.java
@@ -23,9 +23,11 @@
  */
 package de.ozgcloud.nachrichten.postfach.osi;
 
+import de.ozgcloud.nachrichten.file.AttachmentFileTestFactory;
+
 public class MessageAttachmentTestFactory {
 
-	public final static String FILENAME = "test.txt";
+	public final static String FILENAME = AttachmentFileTestFactory.NAME;
 	public static final String CONTENT = "dGVzdA==";
 	public static final String DECODED_CONTENT = "test";
 	public static final long SIZE = 4L;
diff --git a/pom.xml b/pom.xml
index a1265e9ac60cb1fb3c219d3fde293153aae4df77..0e9e340350700284443522feefae9c335268da76 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
 	<parent>
 		<groupId>de.ozgcloud.common</groupId>
 		<artifactId>ozgcloud-common-parent</artifactId>
-		<version>4.5.0</version>
+		<version>4.7.0</version>
 	</parent>
 
 	<groupId>de.ozgcloud.nachrichten</groupId>