Skip to content

Commit

Permalink
feat: add store and copy imap commands. improve structured concurrenc…
Browse files Browse the repository at this point in the history
…y. fix socket reading when it shouldnt
  • Loading branch information
lost-illusi0n committed Dec 30, 2023
1 parent 808fdb9 commit 4d1146c
Show file tree
Hide file tree
Showing 34 changed files with 338 additions and 363 deletions.
42 changes: 41 additions & 1 deletion mailserver/imap-agent/src/commonMain/kotlin/ImapAgent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.sitar.kmail.imap.agent

import dev.sitar.kmail.imap.Capability
import dev.sitar.kmail.imap.agent.transports.ImapServerTransport
import dev.sitar.kmail.imap.frames.DataItem
import dev.sitar.kmail.imap.frames.Tag
import dev.sitar.kmail.imap.frames.command.*
import dev.sitar.kmail.imap.frames.response.*
Expand Down Expand Up @@ -103,6 +104,7 @@ sealed interface State {
StartTlsCommand -> {
agent.transport.send(tag + OkResponse(text = "Let the TLS negotiations begin."))
agent.transport.secure()
logger.debug { "secured ${agent.transport.commandPipeline}" }
}
is AuthenticateCommand -> {
require(command.mechanism is SaslMechanism.Plain)
Expand All @@ -118,6 +120,7 @@ sealed interface State {
agent.transport.send(tag + OkResponse(text = "authenticated."))
agent.state = Authenticated(agent, mailbox)
} else {
logger.debug { "user ${challenge.authenticationIdentity} failed to authenticate."}
TODO("not authenticated")
}
}
Expand Down Expand Up @@ -250,10 +253,18 @@ sealed interface State {
is FetchCommand -> {
fetch(context.command.tag, command)
}
is StoreCommand -> {
store(context.command.tag, command)
}
is CopyCommand -> {
copy(context.command.tag, command)
}
is UidCommand -> {
when (val form = command.command) {
is FetchCommand -> fetch(context.command.tag, form)
else -> error("shouldnt happen, it wouldnt get deserialized.")
is StoreCommand -> store(context.command.tag, form)
is CopyCommand -> copy(context.command.tag, form)
else -> throw Exception("shouldnt happen, it wont get deserialized.")
}
}
is CheckCommand -> {
Expand All @@ -270,6 +281,35 @@ sealed interface State {

agent.transport.send(tag + OkResponse(text = "Here is your mail."))
}

private suspend fun store(tag: Tag, command: StoreCommand) {
val resp = folder.store(command.sequence, command.item.flags.map { Flag.fromValue(it) }.toSet(), command.item.mode)

if (!command.item.silent) {
resp.forEach {
agent.transport.send(Tag.Untagged + FetchResponse(it.key, setOf(DataItem.Response.Flags(it.value.map(Flag::value)))))
}
}

agent.transport.send(tag + OkResponse(text = "Stored new flags."))
}

private suspend fun copy(tag: Tag, command: CopyCommand) {
val messages = folder.sequenceToMessages(command.sequence)

val copy = authenticated.mailbox.folder(command.mailbox)

if (copy == null) {
agent.transport.send(tag + BadResponse(text = "[TRYCREATE] dest doesn't exist."))
return
}

messages.forEach {
copy.save(it.flags, it.typedMessage().asText())
}

agent.transport.send(tag + OkResponse(text = "copy done."))
}
}
class Logout(): State {
override suspend fun handle(context: ImapCommandContext) {
Expand Down
114 changes: 63 additions & 51 deletions mailserver/imap-agent/src/commonMain/kotlin/ImapFolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package dev.sitar.kmail.imap.agent
import dev.sitar.kmail.imap.PartSpecifier
import dev.sitar.kmail.imap.Sequence
import dev.sitar.kmail.imap.frames.DataItem
import dev.sitar.kmail.imap.frames.command.StoreMode
import dev.sitar.kmail.message.Message
import java.util.Collections
import kotlin.math.max
import kotlin.math.min

Expand Down Expand Up @@ -79,62 +79,18 @@ interface ImapFolder {

suspend fun save(flags: Set<Flag>, message: String)

suspend fun update(pos: Int, mode: Sequence.Mode, flags: Set<Flag>)
suspend fun store(sequence: Sequence, flags: Set<Flag>, mode: StoreMode, messagesSnapshot: List<ImapMessage>? = null): Map<Int, Set<Flag>>

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

val start = when (sequence) {
is Sequence.Set -> with(sequence.start) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> TODO()
}
}
is Sequence.Single -> with(sequence.pos) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> TODO()
}
}
}
val selectedMessages = sequenceToMessages(sequence, messagesSnapshot).takeIf { it.isNotEmpty() } ?: return emptyMap()

val end = when (sequence) {
is Sequence.Set -> with(sequence.end) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> messagesSnapshot.size
}
}
is Sequence.Single -> with(sequence.pos) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> TODO()
}
}
}

val exists = exists()

if (!(start in 0..exists && end in 0..exists)) return emptyMap()

val selectedMessages = when (sequence.mode) {
Sequence.Mode.SequenceNumber -> {
messagesSnapshot.subList(messagesSnapshot.size - end, messagesSnapshot.size + 1 - start)
}
Sequence.Mode.Uid -> {
val a = messagesSnapshot.indexOfFirst { it.uniqueIdentifier == start }
val b = messagesSnapshot.indexOfFirst { it.uniqueIdentifier == end }

val start = min(a, b)
val end = max(a, b)
messagesSnapshot.subList(start, end + 1)
}
}
if (dataItems.any { it is DataItem.Fetch.Body }) store(sequence, setOf(Flag.Seen), StoreMode.Add, messagesSnapshot = messagesSnapshot)

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

Expand All @@ -149,8 +105,6 @@ interface ImapFolder {
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 Down Expand Up @@ -186,6 +140,64 @@ interface ImapFolder {
}
}

suspend fun sequenceToMessages(
sequence: Sequence,
messagesSnapshot: List<ImapMessage>? = null
): List<ImapMessage> {
val messagesSnapshot = messagesSnapshot ?: messages()

val start = when (sequence) {
is Sequence.Set -> with(sequence.start) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> TODO()
}
}

is Sequence.Single -> with(sequence.pos) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> TODO()
}
}
}

val end = when (sequence) {
is Sequence.Set -> with(sequence.end) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> messagesSnapshot.size
}
}

is Sequence.Single -> with(sequence.pos) {
when (this) {
is Sequence.Position.Actual -> pos
Sequence.Position.Any -> TODO()
}
}
}

val exists = exists()

if (!(start in 0..exists && end in 0..exists)) return listOf()

return when (sequence.mode) {
Sequence.Mode.Sequence -> {
messagesSnapshot.subList(messagesSnapshot.size - end, messagesSnapshot.size + 1 - start)
}

Sequence.Mode.Uid -> {
val a = messagesSnapshot.indexOfFirst { it.uniqueIdentifier == start }
val b = messagesSnapshot.indexOfFirst { it.uniqueIdentifier == end }

val start = min(a, b)
val end = max(a, b)
messagesSnapshot.subList(start, end + 1)
}
}
}

companion object {
const val DELIM = "/"
}
Expand Down
13 changes: 11 additions & 2 deletions mailserver/imap-agent/src/commonMain/kotlin/ImapServer.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.sitar.kmail.imap.agent

import dev.sitar.kmail.imap.agent.transports.ImapServerTransport
import dev.sitar.kmail.utils.ExceptionLoggingCoroutineExceptionHandler
import dev.sitar.kmail.utils.server.ServerSocket
import kotlinx.coroutines.*
import mu.KotlinLogging
Expand All @@ -14,13 +15,21 @@ class ImapServer(
val layer: ImapLayer
) {
suspend fun listen() = supervisorScope {
logger.debug { "IMAP server is listening." }

while (isActive) {
val transport = ImapServerTransport(socket.accept())

launch {
launch(Dispatchers.IO) {
logger.debug { "Accepted a connection from ${transport.remote}" }

ImapAgent(transport, layer).handle()
try {
ImapAgent(transport, layer).handle()
} catch (e: Exception) {
logger.error(e) { "IMAP session encountered an exception." }
}

logger.debug { "IMAP session completed." }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ class ImapServerTransport(var connection: Connection) {

suspend fun startPipeline() = coroutineScope {
while (isActive && reader.openForRead) {
val command = reader.readCommand()
val context = ImapCommandContext(command, false)
commandPipeline.process(context)
try {
val command = reader.readCommand()
val context = ImapCommandContext(command, false)

commandPipeline.process(context)
} catch (e: Exception) {
logger.error(e) { "imap transport stream encountered exception." }
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion mailserver/imap/src/commonMain/kotlin/Sequence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sealed class Sequence {
data class Set(val start: Position, val end: Position, override val mode: Mode): Sequence()

enum class Mode {
SequenceNumber,
Sequence,
Uid
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.sitar.kmail.imap.frames.command

import dev.sitar.kio.async.readers.AsyncReader
import dev.sitar.kmail.imap.Sequence
import dev.sitar.kmail.imap.readValue

data class CopyCommand(val sequence: Sequence, val mailbox: String): ImapCommand {
override val identifier: ImapCommand.Identifier = ImapCommand.Identifier.Copy

companion object: ImapCommandSerializer<CopyCommand> {
suspend fun deserialize(mode: Sequence.Mode, input: AsyncReader): CopyCommand {
return CopyCommand(Sequence.deserialize(mode, input), input.readValue(isEnd = true))
}

override suspend fun deserialize(input: AsyncReader): CopyCommand =
deserialize(mode = Sequence.Mode.Sequence, input)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ data class FetchCommand(val sequence: Sequence, val dataItems: List<DataItem.Fet
}

override suspend fun deserialize(input: AsyncReader): FetchCommand =
deserialize(mode = Sequence.Mode.SequenceNumber, input)
deserialize(mode = Sequence.Mode.Sequence, input)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ sealed interface ImapCommand {
Notify(NotifyCommand),
Idle(IdleCommand),
Check(CheckCommand),
Append(AppendCommand);
Append(AppendCommand),
Store(StoreCommand),
Copy(CopyCommand);

companion object {
fun findByIdentifier(identifier: String) : Identifier? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dev.sitar.kmail.imap.frames.command

import dev.sitar.kio.async.readers.AsyncReader
import dev.sitar.kmail.imap.Sequence
import dev.sitar.kmail.imap.readList
import dev.sitar.kmail.utils.io.readUtf8StringUntil
import java.lang.Exception

data class StoreCommand(val sequence: Sequence, val item: StoreDataItem): ImapCommand {
override val identifier: ImapCommand.Identifier = ImapCommand.Identifier.Append

companion object: ImapCommandSerializer<StoreCommand> {
suspend fun deserialize(mode: Sequence.Mode, input: AsyncReader): StoreCommand {
val sequence = Sequence.deserialize(mode, input)

val rawName = input.readUtf8StringUntil { it == ' ' }
val flags = input.readList()

val parts = rawName.split('.')

val mode: StoreMode = when (parts[0]) {
"FLAGS" -> StoreMode.Set
"+FLAGS" -> StoreMode.Add
"-FLGS" -> StoreMode.Remove
else -> throw Exception("could not parse store mode ${parts[0]}")
}

val silent = parts.getOrNull(1)?.lowercase()?.toBooleanStrictOrNull() ?: false

return StoreCommand(sequence, StoreDataItem(mode, silent, flags))
}

override suspend fun deserialize(input: AsyncReader): StoreCommand =
deserialize(mode = Sequence.Mode.Sequence, input)
}
}

enum class StoreMode {
Set,
Add,
Remove;
}

data class StoreDataItem(val mode: StoreMode, var silent: Boolean, val flags: List<String>)
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ data class UidCommand(val command: ImapCommand): ImapCommand {

val command = when (ImapCommand.Identifier.findByIdentifier(identifier)) {
ImapCommand.Identifier.Fetch -> FetchCommand.deserialize(Sequence.Mode.Uid, input)
ImapCommand.Identifier.Store -> StoreCommand.deserialize(Sequence.Mode.Uid, input)
ImapCommand.Identifier.Copy -> CopyCommand.deserialize(Sequence.Mode.Uid, input)
// ImapCommand.Identifier.Search
else -> TODO("got $identifier")
}
Expand Down
Loading

0 comments on commit 4d1146c

Please sign in to comment.