diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
index 1c8453b8516351a225252ef6f31941bc5acb41ad..77e9ac4fbf2d91e2dc38ccf50a5d2c2ebeba0412 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
@@ -3,8 +3,10 @@ package org.briarproject.mailbox.core.files
 import io.ktor.application.ApplicationCall
 import io.ktor.auth.principal
 import io.ktor.http.HttpStatusCode
+import io.ktor.request.receiveStream
 import io.ktor.response.respond
-import org.briarproject.mailbox.core.db.Database
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import org.briarproject.mailbox.core.server.AuthException
 import org.briarproject.mailbox.core.server.AuthManager
 import org.briarproject.mailbox.core.server.MailboxPrincipal
@@ -13,8 +15,8 @@ import org.briarproject.mailbox.core.system.RandomIdManager
 import javax.inject.Inject
 
 class FileManager @Inject constructor(
-    private val db: Database,
     private val authManager: AuthManager,
+    private val fileProvider: FileProvider,
     private val randomIdManager: RandomIdManager,
 ) {
 
@@ -32,9 +34,19 @@ class FileManager @Inject constructor(
         randomIdManager.assertIsRandomId(folderId)
         authManager.assertCanPostToFolder(principal, folderId)
 
-        // TODO implement
-
-        call.respond(HttpStatusCode.OK, "post: Not yet implemented. folderId: $folderId}")
+        val fileId = randomIdManager.getNewRandomId()
+        withContext(Dispatchers.IO) {
+            val tmpFile = fileProvider.getTemporaryFile(fileId)
+            tmpFile.outputStream().use { outputStream ->
+                call.receiveStream().use { inputStream ->
+                    inputStream.copyTo(outputStream)
+                }
+            }
+            val file = fileProvider.getFile(folderId, fileId)
+            if (!tmpFile.renameTo(file)) error("Error moving file")
+        }
+
+        call.respond(HttpStatusCode.OK)
     }
 
     /**
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1b72883219e6b6a88c9808c0444976e8344ad8a2
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt
@@ -0,0 +1,66 @@
+package org.briarproject.mailbox.core.files
+
+import io.ktor.client.request.post
+import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.readText
+import io.ktor.http.HttpStatusCode
+import kotlinx.coroutines.runBlocking
+import org.briarproject.mailbox.core.TestUtils.getNewRandomId
+import org.briarproject.mailbox.core.server.IntegrationTest
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.TestInstance.Lifecycle
+import kotlin.random.Random
+import kotlin.test.assertEquals
+
+@TestInstance(Lifecycle.PER_CLASS)
+class FileManagerIntegrationTest : IntegrationTest() {
+
+    private val bytes = Random.nextBytes(2048)
+
+    override fun initDb() {
+        addOwnerToken()
+        addContact(contact1)
+        addContact(contact2)
+    }
+
+    @Test
+    fun `post new file rejects wrong token`(): Unit = runBlocking {
+        val response: HttpResponse = httpClient.post("$baseUrl/files/${getNewRandomId()}") {
+            authenticateWithToken(token)
+            body = bytes
+        }
+        assertEquals(HttpStatusCode.Unauthorized.value, response.status.value)
+    }
+
+    @Test
+    fun `post new file rejects unauthorized folder ID`(): Unit = runBlocking {
+        val response: HttpResponse = httpClient.post("$baseUrl/files/${contact1.outboxId}") {
+            authenticateWithToken(ownerToken)
+            body = bytes
+        }
+        assertEquals(HttpStatusCode.Unauthorized.value, response.status.value)
+    }
+
+    @Test
+    fun `post new file rejects invalid folder ID`(): Unit = runBlocking {
+        val response: HttpResponse = httpClient.post("$baseUrl/files/foo") {
+            authenticateWithToken(ownerToken)
+            body = bytes
+        }
+        assertEquals(HttpStatusCode.BadRequest.value, response.status.value)
+        assertEquals("Malformed ID: foo", response.readText())
+    }
+
+    @Test
+    fun `post new file creates new file`(): Unit = runBlocking {
+        val response: HttpResponse = httpClient.post("$baseUrl/files/${contact1.inboxId}") {
+            authenticateWithToken(ownerToken)
+            body = bytes
+        }
+        assertEquals(HttpStatusCode.OK.value, response.status.value)
+
+        // TODO fetch the file later to see that it was uploaded correctly
+    }
+
+}
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0d21736ef398bdf42db5790546a4a2b17c04bfa6
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt
@@ -0,0 +1,65 @@
+package org.briarproject.mailbox.core.files
+
+import io.ktor.application.ApplicationCall
+import io.ktor.auth.principal
+import io.ktor.http.HttpStatusCode
+import io.ktor.request.receiveStream
+import io.ktor.response.respond
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import kotlinx.coroutines.runBlocking
+import org.briarproject.mailbox.core.TestUtils.getNewRandomId
+import org.briarproject.mailbox.core.server.AuthManager
+import org.briarproject.mailbox.core.server.MailboxPrincipal
+import org.briarproject.mailbox.core.system.RandomIdManager
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.io.ByteArrayInputStream
+import java.io.File
+import kotlin.random.Random
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class FileManagerTest {
+
+    private val authManager: AuthManager = mockk()
+    private val fileProvider: FileProvider = mockk()
+    private val randomIdManager = RandomIdManager()
+
+    private val fileManager = FileManager(authManager, fileProvider, randomIdManager)
+
+    private val call: ApplicationCall = mockk()
+    private val id = getNewRandomId()
+    private val bytes = Random.nextBytes(2048)
+
+    init {
+        mockkStatic("io.ktor.auth.AuthenticationKt")
+        mockkStatic("io.ktor.request.ApplicationReceiveFunctionsKt")
+        mockkStatic("io.ktor.response.ApplicationResponseFunctionsKt")
+    }
+
+    @Test
+    fun `post new file stores file correctly`(@TempDir tempDir: File) = runBlocking {
+        val tmpFile = File(tempDir, "tmp")
+        val finalFile = File(tempDir, "final")
+
+        every { call.principal<MailboxPrincipal>() } returns MailboxPrincipal.OwnerPrincipal
+        every { authManager.assertCanPostToFolder(MailboxPrincipal.OwnerPrincipal, id) } just Runs
+        every { fileProvider.getTemporaryFile(any()) } returns tmpFile
+        coEvery { call.receiveStream() } returns ByteArrayInputStream(bytes)
+        every { fileProvider.getFile(id, any()) } returns finalFile
+        coEvery { call.respond(HttpStatusCode.OK) } just Runs
+
+        fileManager.postFile(call, id)
+
+        assertFalse(tmpFile.exists())
+        assertTrue(finalFile.exists())
+        assertArrayEquals(bytes, finalFile.readBytes())
+    }
+
+}