diff --git a/.gitignore b/.gitignore index bc1cae3..156ebe6 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/src/main/kotlin/com/notkamui/kourrier/core/core.kt b/src/main/kotlin/com/notkamui/kourrier/core/core.kt index 5547edd..e93c6fd 100644 --- a/src/main/kotlin/com/notkamui/kourrier/core/core.kt +++ b/src/main/kotlin/com/notkamui/kourrier/core/core.kt @@ -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, ) diff --git a/src/main/kotlin/com/notkamui/kourrier/core/exceptions.kt b/src/main/kotlin/com/notkamui/kourrier/core/exceptions.kt new file mode 100644 index 0000000..f1a19da --- /dev/null +++ b/src/main/kotlin/com/notkamui/kourrier/core/exceptions.kt @@ -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() diff --git a/src/main/kotlin/com/notkamui/kourrier/imap/folder.kt b/src/main/kotlin/com/notkamui/kourrier/imap/folder.kt index 8fe0a68..6bca6b3 100644 --- a/src/main/kotlin/com/notkamui/kourrier/imap/folder.kt +++ b/src/main/kotlin/com/notkamui/kourrier/imap/folder.kt @@ -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 @@ -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. */ @@ -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. @@ -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) diff --git a/src/main/kotlin/com/notkamui/kourrier/imap/folder_listener.kt b/src/main/kotlin/com/notkamui/kourrier/imap/folder_listener.kt new file mode 100644 index 0000000..1b8fab6 --- /dev/null +++ b/src/main/kotlin/com/notkamui/kourrier/imap/folder_listener.kt @@ -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 */ + } +} diff --git a/src/main/kotlin/com/notkamui/kourrier/imap/imap.kt b/src/main/kotlin/com/notkamui/kourrier/imap/imap.kt index 8e5ab2d..30236da 100644 --- a/src/main/kotlin/com/notkamui/kourrier/imap/imap.kt +++ b/src/main/kotlin/com/notkamui/kourrier/imap/imap.kt @@ -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). @@ -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, @@ -94,6 +172,5 @@ fun Kourrier.imap( debugMode, enableSSL, ).run { - imap(this, properties, callback) + imap(this, properties) } -} diff --git a/src/main/kotlin/com/notkamui/kourrier/imap/message.kt b/src/main/kotlin/com/notkamui/kourrier/imap/message.kt index 34a29ed..21d1bd7 100644 --- a/src/main/kotlin/com/notkamui/kourrier/imap/message.kt +++ b/src/main/kotlin/com/notkamui/kourrier/imap/message.kt @@ -2,28 +2,52 @@ package com.notkamui.kourrier.imap import com.sun.mail.imap.IMAPFolder import com.sun.mail.imap.IMAPMessage +import javax.mail.Header import javax.mail.internet.MimeMultipart /** * Wrapper around the standard [IMAPMessage]. */ class KourrierIMAPMessage(private val message: IMAPMessage) { + /** + * UID of the message. + */ val uid: Long by lazy { - message.messageUID + (message.folder as IMAPFolder).getUID(message) } + /** + * Sender of the message. + */ val from: String by lazy { message.from[0].toString() } + /** + * List of [KourrierMessageHeader] of the message. + */ val headers: List by lazy { message.allHeaders.toList().map { KourrierMessageHeader(it.name, it.value) } } + /** + * Subject of the message. + */ + val subject: String by lazy { + message.subject + } + + /** + * Body of the message. Joined in one [String] even if multipart. + */ val body: String by lazy { bodyParts.joinToString("\n") } + /** + * Body of the message, with each part being a list entry. + * (If the message is not multipart, the length will be 1) + */ val bodyParts: List by lazy { val content = message.content if (content is MimeMultipart) { @@ -36,7 +60,17 @@ class KourrierIMAPMessage(private val message: IMAPMessage) { } } -data class KourrierMessageHeader(val name: String, val value: String) +/** + * Wrapper around the standard [Header]. + */ +data class KourrierMessageHeader internal constructor( + /** + * Name of the header. + */ + val name: String, -private val IMAPMessage.messageUID: Long - get() = (folder as IMAPFolder).getUID(this) + /** + * Value of the header. + */ + val value: String, +) diff --git a/src/main/kotlin/com/notkamui/kourrier/search/search.kt b/src/main/kotlin/com/notkamui/kourrier/search/search.kt index b0baa5c..458771b 100644 --- a/src/main/kotlin/com/notkamui/kourrier/search/search.kt +++ b/src/main/kotlin/com/notkamui/kourrier/search/search.kt @@ -3,53 +3,95 @@ package com.notkamui.kourrier.search import java.util.Date import javax.mail.search.AndTerm import javax.mail.search.ComparisonTerm -import javax.mail.search.NotTerm import javax.mail.search.OrTerm import javax.mail.search.ReceivedDateTerm import javax.mail.search.SearchTerm import javax.mail.search.SentDateTerm import javax.mail.search.SizeTerm +/** + * Interface wrapper to build comparable JavaMail search terms. + * + * [T] is a [Comparable]. + * [R] is a JavaMail [SearchTerm]. + */ +sealed interface KourrierComparableTerm, R : SearchTerm> { + /** + * Equal to [it]. + */ + fun eq(it: T): R + + /** + * Not equal to [it]. + */ + fun ne(it: T): R + + /** + * Less than [it]. + */ + fun lt(it: T): R + + /** + * Less than or equal to [it]. + */ + fun le(it: T): R + + /** + * Greater than [it]. + */ + fun gt(it: T): R + + /** + * Greater than or equal to [it]. + */ + fun ge(it: T): R + + /** + * In closed [range]. + */ + fun inRange(range: ClosedRange): AndTerm +} + /** * Builder wrapper around the standard [ReceivedDateTerm]. */ -object KourrierReceivedDate { - fun eq(date: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.EQ, date) - fun ne(date: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.NE, date) - fun lt(date: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.LT, date) - fun le(date: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.LE, date) - fun gt(date: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.GT, date) - fun ge(date: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.GE, date) - fun inRange(dateRange: ClosedRange): AndTerm = - ge(dateRange.start) and le(dateRange.endInclusive) +object KourrierReceivedDate : KourrierComparableTerm { + override fun eq(it: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.EQ, it) + override fun ne(it: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.NE, it) + override fun lt(it: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.LT, it) + override fun le(it: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.LE, it) + override fun gt(it: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.GT, it) + override fun ge(it: Date): ReceivedDateTerm = ReceivedDateTerm(ComparisonTerm.GE, it) + override fun inRange(range: ClosedRange): AndTerm = + ge(range.start) and le(range.endInclusive) } /** * Builder wrapper around the standard [SentDateTerm]. */ -object KourrierSentDate { - fun eq(date: Date): SentDateTerm = SentDateTerm(ComparisonTerm.EQ, date) - fun ne(date: Date): SentDateTerm = SentDateTerm(ComparisonTerm.NE, date) - fun lt(date: Date): SentDateTerm = SentDateTerm(ComparisonTerm.LT, date) - fun le(date: Date): SentDateTerm = SentDateTerm(ComparisonTerm.LE, date) - fun gt(date: Date): SentDateTerm = SentDateTerm(ComparisonTerm.GT, date) - fun ge(date: Date): SentDateTerm = SentDateTerm(ComparisonTerm.GE, date) - fun inRange(dateRange: ClosedRange): AndTerm = - ge(dateRange.start) and le(dateRange.endInclusive) +object KourrierSentDate : KourrierComparableTerm { + override fun eq(it: Date): SentDateTerm = SentDateTerm(ComparisonTerm.EQ, it) + override fun ne(it: Date): SentDateTerm = SentDateTerm(ComparisonTerm.NE, it) + override fun lt(it: Date): SentDateTerm = SentDateTerm(ComparisonTerm.LT, it) + override fun le(it: Date): SentDateTerm = SentDateTerm(ComparisonTerm.LE, it) + override fun gt(it: Date): SentDateTerm = SentDateTerm(ComparisonTerm.GT, it) + override fun ge(it: Date): SentDateTerm = SentDateTerm(ComparisonTerm.GE, it) + override fun inRange(range: ClosedRange): AndTerm = + ge(range.start) and le(range.endInclusive) } /** * Builder wrapper around the standard [SizeTerm]. */ -object KourrierSize { - fun eq(size: Int): SizeTerm = SizeTerm(ComparisonTerm.EQ, size) - fun ne(size: Int): SizeTerm = SizeTerm(ComparisonTerm.NE, size) - fun lt(size: Int): SizeTerm = SizeTerm(ComparisonTerm.LT, size) - fun le(size: Int): SizeTerm = SizeTerm(ComparisonTerm.LE, size) - fun gt(size: Int): SizeTerm = SizeTerm(ComparisonTerm.GT, size) - fun ge(size: Int): SizeTerm = SizeTerm(ComparisonTerm.GE, size) - fun inRange(sizeRange: IntRange): AndTerm = - ge(sizeRange.first) and le(sizeRange.last) +object KourrierSize : KourrierComparableTerm { + override fun eq(it: Int): SizeTerm = SizeTerm(ComparisonTerm.EQ, it) + override fun ne(it: Int): SizeTerm = SizeTerm(ComparisonTerm.NE, it) + override fun lt(it: Int): SizeTerm = SizeTerm(ComparisonTerm.LT, it) + override fun le(it: Int): SizeTerm = SizeTerm(ComparisonTerm.LE, it) + override fun gt(it: Int): SizeTerm = SizeTerm(ComparisonTerm.GT, it) + override fun ge(it: Int): SizeTerm = SizeTerm(ComparisonTerm.GE, it) + override fun inRange(range: ClosedRange): AndTerm = + ge(range.start) and le(range.endInclusive) } /** @@ -61,8 +103,3 @@ infix fun SearchTerm.and(other: SearchTerm): AndTerm = AndTerm(this, other) * Creates an [OrTerm] of [this] received term and [other] term. */ infix fun SearchTerm.or(other: SearchTerm): OrTerm = OrTerm(this, other) - -/** - * Creates an [NotTerm] of [this] received term. - */ -operator fun SearchTerm.not(): NotTerm = NotTerm(this) diff --git a/src/main/kotlin/com/notkamui/kourrier/search/search_builder.kt b/src/main/kotlin/com/notkamui/kourrier/search/search_builder.kt index 390eb91..b934678 100644 --- a/src/main/kotlin/com/notkamui/kourrier/search/search_builder.kt +++ b/src/main/kotlin/com/notkamui/kourrier/search/search_builder.kt @@ -28,7 +28,7 @@ import javax.mail.search.SubjectTerm /** * DSL builder for creating a valid [SearchTerm]. */ -class KourrierSearch { +class KourrierSearch internal constructor() { private val terms = mutableListOf() private val sortedBy = mutableSetOf() diff --git a/src/main/kotlin/com/notkamui/kourrier/search/sort.kt b/src/main/kotlin/com/notkamui/kourrier/search/sort.kt index 7a71ca0..58f5943 100644 --- a/src/main/kotlin/com/notkamui/kourrier/search/sort.kt +++ b/src/main/kotlin/com/notkamui/kourrier/search/sort.kt @@ -1,5 +1,6 @@ package com.notkamui.kourrier.search +import com.notkamui.kourrier.core.UnknownSortTermException import com.sun.mail.imap.SortTerm /** @@ -17,15 +18,16 @@ enum class KourrierSortTerm(private val rawSortTerm: SortTerm) { ; companion object { + /** + * Obtain a [KourrierSortTerm] from a raw [SortTerm] + */ fun fromRawSortTerm(rawSortTerm: SortTerm): KourrierSortTerm = values().find { it.rawSortTerm == rawSortTerm } ?: throw UnknownSortTermException() } + /** + * Obtain a raw [SortTerm] from this [KourrierSortTerm] + */ fun toRawSortTerm(): SortTerm = rawSortTerm } - -/** - * Is thrown when a [KourrierSortTerm] is unknown or invalid. - */ -class UnknownSortTermException : Exception() diff --git a/src/main/kotlin/com/notkamui/kourrier/search/sort_builder.kt b/src/main/kotlin/com/notkamui/kourrier/search/sort_builder.kt index 007a2c5..bea795c 100644 --- a/src/main/kotlin/com/notkamui/kourrier/search/sort_builder.kt +++ b/src/main/kotlin/com/notkamui/kourrier/search/sort_builder.kt @@ -2,9 +2,15 @@ package com.notkamui.kourrier.search import com.sun.mail.imap.SortTerm -class KourrierSort { +/** + * DSL builder for creating a valid list of [KourrierSortTerm]s ([SortTerm]). + */ +class KourrierSort internal constructor() { private val sortTerms = mutableListOf() + /** + * Builds the sort engine to be used to fetch messages. + */ fun build(): List = sortTerms private fun add(sortTerm: KourrierSortTerm) { @@ -16,19 +22,31 @@ class KourrierSort { sortTerms.add(sortTerm) } + /** + * Adds a [KourrierSortTerm] in the natural order direction. + */ operator fun KourrierSortTerm.unaryPlus() { add(this) } + /** + * Adds a [KourrierSortTerm] in the reverse order direction. + */ operator fun KourrierSortTerm.not() { add(KourrierSortTerm.Reverse) add(this) } + /** + * Adds a [SortTerm] in the natural order direction. + */ operator fun SortTerm.unaryPlus() { add(this) } + /** + * Adds a [SortTerm] in the reverse order direction. + */ operator fun SortTerm.not() { add(KourrierSortTerm.Reverse) add(this)