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 f1227f376debabac67424083d2854513be741769..84089d12d3263e01e99e8e287071da18b0c6021d 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
@@ -5,6 +5,7 @@ import io.ktor.auth.principal
 import io.ktor.http.HttpStatusCode
 import io.ktor.request.receiveStream
 import io.ktor.response.respond
+import io.ktor.response.respondFile
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import org.briarproject.mailbox.core.server.AuthException
@@ -83,12 +84,9 @@ class FileManager @Inject constructor(
         randomIdManager.assertIsRandomId(fileId)
         authManager.assertCanDownloadFromFolder(principal, folderId)
 
-        // TODO implement
-
-        call.respond(
-            HttpStatusCode.OK,
-            "get: Not yet implemented. folderId: $folderId fileId: $fileId"
-        )
+        val file = fileProvider.getFile(folderId, fileId)
+        if (file.isFile) call.respondFile(file)
+        else call.respond(HttpStatusCode.NotFound)
     }
 
     /**
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
index 25d5a739c50c47da69e8aac4731b8c561d9d4648..7145be38e6e68a2dcf7793df4a6ce640bf0b06d8 100644
--- 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
@@ -4,11 +4,13 @@ import io.ktor.client.call.receive
 import io.ktor.client.request.get
 import io.ktor.client.request.post
 import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.readBytes
 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.Assertions.assertArrayEquals
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.TestInstance
 import org.junit.jupiter.api.TestInstance.Lifecycle
@@ -71,8 +73,14 @@ class FileManagerIntegrationTest : IntegrationTest() {
         val fileList: FileListResponse = listResponse.receive()
         assertEquals(1, fileList.files.size)
 
-        // TODO fetch the file later to see that it was uploaded correctly
+        // contact can download the file
         val fileId = fileList.files[0].name
+        val fileResponse: HttpResponse =
+            httpClient.get("$baseUrl/files/${contact1.inboxId}/$fileId") {
+                authenticateWithToken(contact1.token)
+            }
+        assertEquals(HttpStatusCode.OK.value, fileResponse.status.value)
+        assertArrayEquals(bytes, fileResponse.readBytes())
 
         // TODO delete the file to clean up again
     }
@@ -115,4 +123,53 @@ class FileManagerIntegrationTest : IntegrationTest() {
         assertEquals("""{"files":[]}""", response.readText())
     }
 
+    @Test
+    fun `get file rejects wrong token`(): Unit = runBlocking {
+        val response: HttpResponse =
+            httpClient.get("$baseUrl/files/${getNewRandomId()}/${getNewRandomId()}") {
+                authenticateWithToken(token)
+                body = bytes
+            }
+        assertEquals(HttpStatusCode.Unauthorized.value, response.status.value)
+    }
+
+    @Test
+    fun `get file rejects unauthorized folder ID`(): Unit = runBlocking {
+        val response: HttpResponse =
+            httpClient.get("$baseUrl/files/${contact1.inboxId}/${getNewRandomId()}") {
+                authenticateWithToken(ownerToken)
+                body = bytes
+            }
+        assertEquals(HttpStatusCode.Unauthorized.value, response.status.value)
+    }
+
+    @Test
+    fun `get file rejects invalid folder ID`(): Unit = runBlocking {
+        val response: HttpResponse = httpClient.get("$baseUrl/files/foo/${getNewRandomId()}") {
+            authenticateWithToken(ownerToken)
+            body = bytes
+        }
+        assertEquals(HttpStatusCode.BadRequest.value, response.status.value)
+        assertEquals("Malformed ID: foo", response.readText())
+    }
+
+    @Test
+    fun `get file rejects invalid file ID`(): Unit = runBlocking {
+        val response: HttpResponse = httpClient.get("$baseUrl/files/${contact1.outboxId}/bar") {
+            authenticateWithToken(ownerToken)
+            body = bytes
+        }
+        assertEquals(HttpStatusCode.BadRequest.value, response.status.value)
+        assertEquals("Malformed ID: bar", response.readText())
+    }
+
+    @Test
+    fun `get file gives 404 response for unknown file`(): Unit = runBlocking {
+        val id = getNewRandomId()
+        val response: HttpResponse = httpClient.get("$baseUrl/files/${contact1.outboxId}/$id") {
+            authenticateWithToken(ownerToken)
+            body = bytes
+        }
+        assertEquals(HttpStatusCode.NotFound.value, response.status.value)
+    }
 }