Skip to content

Commit

Permalink
wip: step towards new fs and flags.
Browse files Browse the repository at this point in the history
  • Loading branch information
lost-illusi0n committed Nov 21, 2023
1 parent eadedac commit d196bdd
Show file tree
Hide file tree
Showing 20 changed files with 280 additions and 80 deletions.
6 changes: 3 additions & 3 deletions mailserver/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
kotlin("jvm") version "1.8.0" apply false
kotlin("multiplatform") version "1.8.0" apply false
kotlin("plugin.serialization") version "1.8.0" apply false
kotlin("jvm") version "1.8.20" apply false
kotlin("multiplatform") version "1.8.20" apply false
kotlin("plugin.serialization") version "1.8.20" apply false
id("kotlinx-atomicfu") apply false
}

Expand Down
Binary file modified mailserver/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
4 changes: 2 additions & 2 deletions mailserver/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStorePath=wrapper/dists
6 changes: 4 additions & 2 deletions mailserver/imap-agent/src/commonMain/kotlin/ImapAgent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,14 @@ sealed interface State {
fetch(context.command.tag, command)
}
is UidCommand -> {
val form = command.command
when (form) {
when (val form = command.command) {
is FetchCommand -> fetch(context.command.tag, form)
else -> error("shouldnt happen, it wouldnt get deserialized.")
}
}
is CheckCommand -> {
agent.transport.send(context.command.tag + OkResponse(text = "noop"))
}
else -> authenticated.handle(context) // authenticated commands are allowed here
}
}
Expand Down
30 changes: 26 additions & 4 deletions mailserver/imap-agent/src/commonMain/kotlin/ImapFolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,27 @@ interface ImapMailbox {
suspend fun subscribe(folder: String)
}

sealed class Flag(val value: String) {
object Replied: Flag("\\ANSWERED")
object Seen: Flag("\\SEEN")
object Trashed: Flag("\\DELETED")
object Draft: Flag("\\DRAFT")
object Flagged: Flag("\\FLAGGED")
object Recent: Flag("\\RECENT")
class Other(value: String): Flag(value)
}

interface ImapMessage {
val uniqueIdentifier: Int
val sequenceNumber: Int
val flags: Set<Flag>

val size: Long

suspend fun typedMessage(): Message
}

// TODO: maybe allow to lock?
interface ImapFolder {
val name: String

Expand All @@ -50,6 +62,8 @@ interface ImapFolder {

suspend fun messages(): List<ImapMessage>

suspend fun update(pos: Int, mode: Sequence.Mode, flags: Set<Flag>)

suspend fun fetch(sequence: Sequence, dataItems: List<DataItem.Fetch>): Map<Int, Set<DataItem.Response>> {
val messagesSnapshot = messages()

Expand Down Expand Up @@ -102,20 +116,24 @@ interface ImapFolder {
}

return selectedMessages.associate { message ->
when (sequence.mode) {
val pos = when (sequence.mode) {
Sequence.Mode.SequenceNumber -> message.sequenceNumber
Sequence.Mode.Uid -> message.uniqueIdentifier
} to buildSet {
}

pos to buildSet {
// if sequence is UID the UID response is implicit
if (sequence.mode == Sequence.Mode.Uid && DataItem.Fetch.Uid !in dataItems) {
add(DataItem.Response.Uid(message.uniqueIdentifier.toString()))
}

for (item in dataItems) when (item) {
DataItem.Fetch.Flags -> add(DataItem.Response.Flags(listOf("\\Recent")))
DataItem.Fetch.Flags -> add(DataItem.Response.Flags(message.flags.map { it.value }))
DataItem.Fetch.Rfc822Size -> add(DataItem.Response.Rfc822Size(message.size))
DataItem.Fetch.Uid -> add(DataItem.Response.Uid(message.uniqueIdentifier.toString()))
is DataItem.Fetch.BodyType -> {
if (item is DataItem.Fetch.Body) update(pos, sequence.mode, setOf(Flag.Seen))

val typed = message.typedMessage()

if (item.parts.isEmpty()) {
Expand All @@ -137,9 +155,13 @@ interface ImapFolder {
)
)

is PartSpecifier.Fetch.Header -> {
PartSpecifier.Fetch.Header -> {
add(DataItem.Response.Body(PartSpecifier.Response.Header(typed.headers.toList())))
}

PartSpecifier.Fetch.Text -> {
add(DataItem.Response.Body(PartSpecifier.Response.Text(typed.body ?: "")))
}
}
}
}
Expand Down
22 changes: 21 additions & 1 deletion mailserver/imap/src/commonMain/kotlin/PartSpecifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ sealed interface PartSpecifier {

enum class Identifier(val raw: String, val fetchSerializer: Fetch.Serializer<out Fetch>) {
HeaderFields("HEADER.FIELDS", Fetch.HeaderFields),
Header("HEADER", Fetch.Header);
Header("HEADER", Fetch.Header),
Text("TEXT", Fetch.Text);

companion object {
fun from(raw: String): Identifier? {
Expand Down Expand Up @@ -40,6 +41,14 @@ sealed interface PartSpecifier {
}
}

object Text: Fetch, Serializer<Text> {
override val identifier: Identifier = Identifier.Text

override suspend fun deserialize(input: AsyncReader): Text {
return Text
}
}

// object Header: Fetch, Serializer<Header> {
// override val identifier: Identifier = Identifier.Header
//
Expand Down Expand Up @@ -88,6 +97,17 @@ sealed interface PartSpecifier {
}
}

data class Text(val text: String): Response {
override val isInline: Boolean
get() = false

override suspend fun serializeInline(output: AsyncWriter) { }

override suspend fun serializeBody(output: AsyncWriter) {
output.writeStringUtf8(text)
}
}

data class Body(val message: Message): Response {
override val isInline: Boolean = false

Expand Down
1 change: 1 addition & 0 deletions mailserver/imap/src/commonMain/kotlin/Sequence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ sealed class Sequence {
abstract val mode: Mode

data class Single(val pos: Position, override val mode: Mode): Sequence()

data class Set(val start: Position, val end: Position, override val mode: Mode): Sequence()

enum class Mode {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.sitar.kmail.imap.frames.command

import dev.sitar.kio.async.readers.AsyncReader

object CheckCommand : ImapCommand, ImapCommandSerializer<CheckCommand> {
override val identifier = ImapCommand.Identifier.Check

override suspend fun deserialize(input: AsyncReader): CheckCommand {
return CheckCommand
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ sealed interface ImapCommand {
Create(CreateCommand),
Status(StatusCommand),
Notify(NotifyCommand),
Idle(IdleCommand);
Idle(IdleCommand),
Check(CheckCommand);

companion object {
fun findByIdentifier(identifier: String) : Identifier? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ data class UidCommand(val command: ImapCommand): ImapCommand {
}

override suspend fun deserialize(input: AsyncReader): UidCommand {
val command = when (ImapCommand.Identifier.findByIdentifier(input.readCommandIdentifier())) {
val identifier = input.readCommandIdentifier()

val command = when (ImapCommand.Identifier.findByIdentifier(identifier)) {
ImapCommand.Identifier.Fetch -> FetchCommand.deserialize(Sequence.Mode.Uid, input)
// ImapCommand.Identifier.Search
else -> TODO("bad syntax")
else -> TODO("got $identifier")
}

return UidCommand(command)
Expand Down
31 changes: 27 additions & 4 deletions mailserver/runner/src/main/kotlin/imap.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package dev.sitar.kmail.runner

import dev.sitar.kmail.imap.Sequence
import dev.sitar.kmail.imap.agent.*
import dev.sitar.kmail.message.Message
import dev.sitar.kmail.runner.storage.*
import dev.sitar.kmail.runner.storage.StorageLayer
import dev.sitar.kmail.runner.storage.formats.Mailbox
import dev.sitar.kmail.runner.storage.formats.MailboxFolder
import dev.sitar.kmail.runner.storage.formats.MailboxMessage
Expand All @@ -12,6 +13,8 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import mu.KotlinLogging
import kotlin.math.max
import kotlin.math.min

private val logger = KotlinLogging.logger { }

Expand All @@ -26,7 +29,13 @@ suspend fun imap(socket: ServerSocketFactory, layer: ImapLayer): ImapServer = co
server
}

class KmailImapMessage(val folder: KmailImapFolder, override val sequenceNumber: Int, override val size: Long, val message: suspend () -> Message) : ImapMessage {
// TODO: this is horrible
class KmailImapMessage(
override val flags: Set<Flag>,
override val sequenceNumber: Int,
override val size: Long,
private val message: suspend () -> Message,
) : ImapMessage {
override val uniqueIdentifier: Int = sequenceNumber

override suspend fun typedMessage(): Message {
Expand Down Expand Up @@ -67,15 +76,29 @@ class KmailImapFolder(val folder: MailboxFolder) : ImapFolder {

override suspend fun messages(): List<ImapMessage> {
return folder.messages().mapIndexed { index, message -> KmailImapMessage(
this,
message.flags,
index + 1,
message.length,
message::getMessage
) }
}

// TODO: mix-matched nullability
suspend fun get(pos: Int, mode: Sequence.Mode): MailboxMessage {
return when (mode) {
Sequence.Mode.SequenceNumber -> folder.message(exists() - pos)
Sequence.Mode.Uid -> folder.messageByUid(pos)!!
}
}

override suspend fun update(pos: Int, mode: Sequence.Mode, flags: Set<Flag>) {
val message = get(pos, mode)

message.updateFlags(flags)
}

override suspend fun onMessageStore(handler: (suspend (ImapMessage) -> Unit)?) {
folder.onMessageStore = { handler?.invoke(KmailImapMessage(this, exists(), it.size.toLong()) { it }) }
folder.onMessageStore = { handler?.invoke(KmailImapMessage(setOf(Flag.Recent), exists(), it.size.toLong(), { it })) }
}
}

Expand Down
2 changes: 2 additions & 0 deletions mailserver/runner/src/main/kotlin/launcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ suspend fun run(serverSocketFactory: ServerSocketFactory, connectionFactory: Con
println(KMAIL_ASCII)
logger.info("Kmail is starting.")

logger.debug { "Detected following configuration: $Config" }

val incoming = MutableSharedFlow<InternetMessage>()
val outgoing = MutableSharedFlow<InternetMessage>()

Expand Down
1 change: 0 additions & 1 deletion mailserver/runner/src/main/kotlin/launcherJvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package dev.sitar.kmail.runner

import dev.sitar.kmail.utils.connection.TlsCapableConnectionFactory
import dev.sitar.kmail.utils.server.TlsCapableServerSocketFactory
import io.ktor.network.tls.*
import kotlinx.coroutines.coroutineScope

suspend fun main(): Unit = coroutineScope {
Expand Down
26 changes: 19 additions & 7 deletions mailserver/runner/src/main/kotlin/storage/filesystems/cached.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class CachedFolder(val folder: FsFolder): FsFolder {
private var hasRetrievedFiles = false
var files = mutableListOf<FsFile>()
private set
var fileContents = mutableMapOf<String, ByteArray>()
private set

override fun folder(name: String): FsFolder {
return folders.find { it.name == name } ?: CachedFolder(folder.folder(name)).also { folders.add(it) }
Expand All @@ -62,12 +64,16 @@ class CachedFolder(val folder: FsFolder): FsFolder {
return CachedFolder(folder.createFolder(name)).also { folders.add(it) }
}

override suspend fun getFile(name: String): FsFile? {
return files.find { it.name == name } ?: folder.getFile(name)?.also { files.add(it) }
}

override suspend fun listFiles(): List<FsFile> {
if (hasRetrievedFiles) {
return files
}
else {
files = folder.listFiles().map { CachedFile(it.name, it.size) { it.readContent() } }.toMutableList()
files = folder.listFiles().toMutableList()
hasRetrievedFiles = true
return files
}
Expand All @@ -83,14 +89,16 @@ class CachedFolder(val folder: FsFolder): FsFolder {
}

override suspend fun readFile(name: String): ByteArray? {
return files.find { it.name == name }?.readContent() ?: folder.readFile(name)?.also { files.add(CachedFile(name, it.size.toLong()) { it }) }
return folder.readFile(name)?.also { fileContents[name] = it }
}

override suspend fun writeFile(name: String, contents: ByteArray): FsFile {
folder.writeFile(name, contents)

val cached = CachedFile(name, contents.size.toLong()) { contents }
val cached = FsFile(name, contents.size.toLong())
files.add(cached)
fileContents[name] = contents

return cached
}

Expand All @@ -103,10 +111,14 @@ class CachedFolder(val folder: FsFolder): FsFolder {
files.remove(file)
folder.files.add(file)
}
}

class CachedFile(override val name: String, override val size: Long, val content: suspend () -> ByteArray) : FsFile {
override suspend fun readContent(): ByteArray {
return content()
override suspend fun rename(from: String, to: String) {
folder.rename(from, to)

val file = files.find { it.name == from } ?: return
files.remove(file)
files.add(FsFile(to, file.size))

fileContents[to] = fileContents.remove(from) ?: return
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface FsFolder: Attributable {

suspend fun createFolder(name: String): FsFolder

suspend fun getFile(name: String): FsFile?

suspend fun listFiles(): List<FsFile>

suspend fun listFolders(): List<FsFolder>
Expand All @@ -23,12 +25,10 @@ interface FsFolder: Attributable {

suspend fun writeFile(name: String, contents: ByteArray): FsFile

// TODO: change this
suspend fun move(file: String, folder: FsFolder)
}

interface FsFile {
val name: String
val size: Long
suspend fun rename(from: String, to: String)
}

suspend fun readContent(): ByteArray
}
data class FsFile(val name: String, val size: Long)
Loading

0 comments on commit d196bdd

Please sign in to comment.