Skip to content

Commit

Permalink
Merge pull request #6 from notKamui/dev
Browse files Browse the repository at this point in the history
IMAP engine rework and folder listeners
  • Loading branch information
notKamui authored Sep 17, 2021
2 parents c60cae3 + 24f7b20 commit 96050cd
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 87 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,6 @@ fabric.properties

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

# main file
main.kt
23 changes: 23 additions & 0 deletions src/main/kotlin/com/notkamui/kourrier/core/core.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,33 @@ object Kourrier
* and SSL can be enabled with [enableSSL] (defaults to true).
*/
data class KourrierConnectionInfo(
/**
* Hostname of the server.
*/
val hostname: String,

/**
* Port to open the connection (e.g. 993).
*/
val port: Int,

/**
* Username which to log in with.
*/
val username: String,

/**
* Password of the user.
*/
val password: String,

/**
* Launch connection in debug mode (defaults to false).
*/
val debugMode: Boolean = false,

/**
* Enable SSL on the connection (defaults to true).
*/
val enableSSL: Boolean = true,
)
30 changes: 30 additions & 0 deletions src/main/kotlin/com/notkamui/kourrier/core/exceptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.notkamui.kourrier.core

import com.notkamui.kourrier.imap.KourrierFolder
import com.notkamui.kourrier.imap.KourrierFolderType
import com.notkamui.kourrier.imap.KourrierIMAPSession
import com.notkamui.kourrier.search.KourrierSortTerm

/**
* Is thrown when an inconsistency in the [KourrierIMAPSession] status happens.
*/
class KourrierIMAPSessionStateException
internal constructor(message: String) : IllegalStateException(message)

/**
* Is thrown when an inconsistent in the [KourrierFolder] status happens.
*/
class KourrierIMAPFolderStateException
internal constructor(message: String) : IllegalStateException(message)

/**
* Is thrown when a [KourrierFolderType] is unknown or invalid.
*/
class UnknownFolderTypeException
internal constructor() : IllegalArgumentException()

/**
* Is thrown when a [KourrierSortTerm] is unknown or invalid.
*/
class UnknownSortTermException
internal constructor() : IllegalArgumentException()
32 changes: 26 additions & 6 deletions src/main/kotlin/com/notkamui/kourrier/imap/folder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.notkamui.kourrier.imap

import com.notkamui.kourrier.core.KourrierFlag
import com.notkamui.kourrier.core.KourrierFlags
import com.notkamui.kourrier.core.KourrierIMAPFolderStateException
import com.notkamui.kourrier.core.UnknownFolderTypeException
import com.notkamui.kourrier.search.KourrierSearch
import com.notkamui.kourrier.search.KourrierSort
import com.sun.mail.imap.IMAPFolder
Expand All @@ -13,9 +15,15 @@ import javax.mail.Message
/**
* Wrapper around the standard [IMAPFolder].
*/
class KourrierFolder(private val imapFolder: IMAPFolder) {
class KourrierFolder internal constructor(internal val imapFolder: IMAPFolder) {
private var profile = FetchProfile()

/**
* The current state of the folder.
*/
val isOpen: Boolean
get() = imapFolder.isOpen

/**
* Amount of messages in the current folder.
*/
Expand Down Expand Up @@ -48,11 +56,28 @@ class KourrierFolder(private val imapFolder: IMAPFolder) {

/**
* Closes the current [IMAPFolder], and [expunge]s it or not (defaults to false).
*
* @throws KourrierIMAPFolderStateException if the folder is already closed.
*/
fun close(expunge: Boolean = false) {
if (!imapFolder.isOpen)
throw KourrierIMAPFolderStateException("Cannot close a folder that is already closed.")

imapFolder.close(expunge)
}

/**
* Opens the current [IMAPFolder] with the given [mode].
*
* @throws KourrierIMAPFolderStateException if the folder is already open.
*/
fun open(mode: KourrierFolderMode) {
if (imapFolder.isOpen)
throw KourrierIMAPFolderStateException("Cannot open a folder that is already open.")

imapFolder.open(mode.toRawFolderMode())
}

/**
* Obtains the message at the given [index],
* or null if it doesn't exist.
Expand Down Expand Up @@ -202,11 +227,6 @@ enum class KourrierFolderType(private val rawType: Int) {
internal fun toRawFolderType(): Int = rawType
}

/**
* Is thrown when a [KourrierFolderType] is unknown or invalid.
*/
class UnknownFolderTypeException : Exception()

private operator fun IMAPFolder.get(index: Int): KourrierIMAPMessage? =
(getMessage(index) as IMAPMessage?)?.let {
KourrierIMAPMessage(it)
Expand Down
49 changes: 49 additions & 0 deletions src/main/kotlin/com/notkamui/kourrier/imap/folder_listener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.notkamui.kourrier.imap

import java.util.EventListener

/**
* [EventListener] interface of [KourrierFolder] to listen to [KourrierIMAPMessage]s.
*/
interface KourrierFolderListener : EventListener {
/**
* Is launched when a [message] is received.
*/
fun onMessageReceived(message: KourrierIMAPMessage)

/**
* Is launched when a [message] is **expunged** (this doesn't always mean "deleted").
*/
fun onMessageRemoved(message: KourrierIMAPMessage)

/**
* Is launched when a [message]'s flags changed.
*/
fun onMessageFlagsChanged(message: KourrierIMAPMessage)

/**
* Is launched when a [message]'s envelope changed.
*/
fun onMessageEnvelopeChanged(message: KourrierIMAPMessage)
}

/**
* Generic adapter for [KourrierFolderListener].
*/
open class KourrierFolderAdapter : KourrierFolderListener {
override fun onMessageReceived(message: KourrierIMAPMessage) {
/* Default */
}

override fun onMessageRemoved(message: KourrierIMAPMessage) {
/* Default */
}

override fun onMessageFlagsChanged(message: KourrierIMAPMessage) {
/* Default */
}

override fun onMessageEnvelopeChanged(message: KourrierIMAPMessage) {
/* Default */
}
}
151 changes: 114 additions & 37 deletions src/main/kotlin/com/notkamui/kourrier/imap/imap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,155 @@ package com.notkamui.kourrier.imap

import com.notkamui.kourrier.core.Kourrier
import com.notkamui.kourrier.core.KourrierConnectionInfo
import com.notkamui.kourrier.core.KourrierIMAPSessionStateException
import com.sun.mail.imap.IMAPFolder
import com.sun.mail.imap.IMAPMessage
import com.sun.mail.imap.IdleManager
import java.io.Closeable
import java.util.Properties
import java.util.concurrent.Executors
import javax.mail.Session
import javax.mail.Store
import javax.mail.event.MessageChangedEvent
import javax.mail.event.MessageCountEvent
import javax.mail.event.MessageCountListener

/**
* Wrapper around JavaMail [Store] session with IMAP helpers
*/
class KourrierIMAPSession internal constructor(
private val connectionInfo: KourrierConnectionInfo,
properties: Properties,
) : Closeable {
private val session: Session
private val store: Store
) : AutoCloseable, Closeable {
private lateinit var idleManager: IdleManager

val isOpen: Boolean
get() = store.isConnected

init {
properties["mail.imap.ssl.enable"] = connectionInfo.enableSSL
properties["mail.imaps.usesocketchannels"] = true
session = Session.getInstance(properties)
session.debug = connectionInfo.debugMode
store = session.getStore(
"imap${
if (connectionInfo.enableSSL) "s"
else ""
}"
)
open()
}

/**
* Closes the current [Store] session (and subsequently disconnects the IMAP session).
* Closes the current [KourrierIMAPSession].
*
* @throws KourrierIMAPSessionStateException if the session is already closed.
*/
override fun close() {
if (!store.isConnected)
throw KourrierIMAPSessionStateException("Cannot close a session that is already closed.")
idleManager.stop()
store.close()
}

/**
* Opens a folder in the current session by its [name],
* Opends the current [KourrierIMAPSession].
*
* @throws KourrierIMAPSessionStateException if the session is already open.
*/
fun open() {
if (store.isConnected)
throw KourrierIMAPSessionStateException("Cannot open a session that is already open.")
idleManager = IdleManager(session, Executors.newCachedThreadPool())
with(connectionInfo) {
store.connect(hostname, port, username, password)
}
}

/**
* Applies the given [callback] to the current [KourrierIMAPSession].
*/
operator fun invoke(callback: KourrierIMAPSession.() -> Unit) {
callback()
}

/**
* Opens and returns a [KourrierFolder] in the current session by its [name],
* with the given [mode],
* and applies the [callback] lambda with itself as the receiver.
*
* Closes the folder without expunging it on exit.
* A [listener] can be given.
*
* **Note that the returned folder should be closed at some point.**
*
* **Note that some mail servers need ReadWrite permissions for a folder to be listenable.**
*
* @throws KourrierIMAPSessionStateException if the session is closed.
*/
fun folder(
name: String,
mode: KourrierFolderMode,
expunge: Boolean = false,
callback: KourrierFolder.() -> Unit
) {
(store.getFolder(name) as IMAPFolder).apply {
open(mode.toRawFolderMode())
KourrierFolder(this).apply {
callback()
close(expunge)
listener: KourrierFolderListener? = null,
callback: KourrierFolder.() -> Unit = {}
): KourrierFolder {
if (!store.isConnected)
throw KourrierIMAPSessionStateException("Cannot interact with a closed session.")

val folder = store.getFolder(name) as IMAPFolder
folder.open(mode.toRawFolderMode())
val kfolder = KourrierFolder(folder)
listener?.let { kfolder.addListener(it) }
kfolder.callback()
return kfolder
}

/**
* Adds a [listener] to a [KourrierFolder].
*/
fun KourrierFolder.addListener(listener: KourrierFolderListener) {
this.imapFolder.addMessageCountListener(object : MessageCountListener {
override fun messagesAdded(event: MessageCountEvent) {
event.messages.forEach {
listener.onMessageReceived(KourrierIMAPMessage(it as IMAPMessage))
}
idleManager.watch(this@addListener.imapFolder)
}

override fun messagesRemoved(event: MessageCountEvent) {
event.messages.forEach {
listener.onMessageRemoved(KourrierIMAPMessage(it as IMAPMessage))
}
idleManager.watch(this@addListener.imapFolder)
}
})

this.imapFolder.addMessageChangedListener { event: MessageChangedEvent ->
val message = KourrierIMAPMessage(event.message as IMAPMessage)
when (event.messageChangeType) {
MessageChangedEvent.FLAGS_CHANGED -> listener.onMessageFlagsChanged(message)
MessageChangedEvent.ENVELOPE_CHANGED -> listener.onMessageEnvelopeChanged(message)
}
idleManager.watch(this.imapFolder)
}

idleManager.watch(this.imapFolder)
}
}

/**
* Opens an IMAP session using the [connectionInfo] and [properties],
* and applies the [callback] lambda with itself as the receiver.
* Opens an IMAP session using the [connectionInfo] and [properties].
*/
fun Kourrier.imap(
connectionInfo: KourrierConnectionInfo,
properties: Properties = Properties(),
callback: KourrierIMAPSession.() -> Unit
) {
properties["mail.imap.ssl.enable"] = connectionInfo.enableSSL
val session = Session.getInstance(properties)
session.debug = connectionInfo.debugMode
val store = session.getStore(
"imap${
if (connectionInfo.enableSSL) "s"
else ""
}"
)
with(connectionInfo) {
store.connect(hostname, port, username, password)
}
KourrierIMAPSession(store).use(callback)
}
): KourrierIMAPSession =
KourrierIMAPSession(connectionInfo, properties)

/**
* Opens an IMAP session using the specified credentials
* ([hostname], [port], [username], [password]) and [properties],
* and applies the [callback] lambda with itself as the receiver.
* ([hostname], [port], [username], [password]) and [properties].
*
* - Debug mode can be enabled with [debugMode] (defaults to false).
* - SSL can be enabled with [enableSSL] (defaults to true).
Expand All @@ -83,9 +162,8 @@ fun Kourrier.imap(
password: String,
debugMode: Boolean = false,
enableSSL: Boolean = true,
properties: Properties = Properties(),
callback: KourrierIMAPSession.() -> Unit
) {
properties: Properties = Properties()
): KourrierIMAPSession =
KourrierConnectionInfo(
hostname,
port,
Expand All @@ -94,6 +172,5 @@ fun Kourrier.imap(
debugMode,
enableSSL,
).run {
imap(this, properties, callback)
imap(this, properties)
}
}
Loading

0 comments on commit 96050cd

Please sign in to comment.