From 654d721428f52a8c387ef356231c6d1d2eb1f801 Mon Sep 17 00:00:00 2001
From: tursom <tursom@foxmail.com>
Date: Sun, 8 Mar 2020 01:48:36 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0mail=20read=E5=BA=93=EF=BC=9B?=
 =?UTF-8?q?=E4=B8=BAParser=E6=B7=BB=E5=8A=A0=E5=AF=B9Set=E7=9A=84=E6=94=AF?=
 =?UTF-8?q?=E6=8C=81=EF=BC=9B=E5=AE=8C=E5=96=84Yaml.toYaml=E6=96=B9?=
 =?UTF-8?q?=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/kotlin/cn/tursom/core/Parser.kt      | 145 +++++++++++-------
 .../main/kotlin/cn/tursom/mail/EmailData.kt   |   1 -
 .../src/main/kotlin/cn/tursom/mail/read.kt    | 120 +++++++++++++++
 .../src/main/kotlin/cn/tursom/yaml/Yaml.kt    | 100 +++++++-----
 4 files changed, 268 insertions(+), 98 deletions(-)
 create mode 100644 utils/mail/src/main/kotlin/cn/tursom/mail/read.kt

diff --git a/src/main/kotlin/cn/tursom/core/Parser.kt b/src/main/kotlin/cn/tursom/core/Parser.kt
index f896207..b89bf1c 100644
--- a/src/main/kotlin/cn/tursom/core/Parser.kt
+++ b/src/main/kotlin/cn/tursom/core/Parser.kt
@@ -10,75 +10,106 @@ object Parser {
 
   fun <T> parse(yaml: Any, clazz: Class<T>): T? {
     @Suppress("UNCHECKED_CAST")
-    return if (yaml is List<*> && clazz.isArray) {
-      parseArray(yaml, clazz) as T
-    } else when (clazz) {
-      Any::class.java -> yaml
-      Int::class.java -> yaml.toInt()
-      Long::class.java -> yaml.toLong()
-      Float::class.java -> yaml.toFloat()
-      Double::class.java -> yaml.toDouble()
-      Boolean::class.java -> yaml.toBoolean()
-
-      getClazz<Int>() -> yaml.toInt()
-      getClazz<Long>() -> yaml.toLong()
-      getClazz<Float>() -> yaml.toFloat()
-      getClazz<Double>() -> yaml.toDouble()
-      getClazz<Boolean>() -> yaml.toBoolean()
-      String::class.java -> yaml.toString()
-
-      else -> {
-        if (yaml !is Map<*, *>) return null
-        val instance = try {
-          clazz.newInstance()
-        } catch (e: Exception) {
-          unsafe.allocateInstance(clazz)
-        }
-        val fields = clazz.declaredFields
-        fields.forEach {
-          if ((it.modifiers and (Modifier.STATIC or Modifier.TRANSIENT)) != 0) return@forEach
-          try {
-            val parse = parseField(yaml[it.name] ?: return@forEach, it) ?: return@forEach
-            it.isAccessible = true
-            it.set(instance, parse)
-          } catch (e: Exception) {
-          }
-        }
-        instance
+    return when {
+      clazz.isInstance(yaml) -> yaml.cast()
+      clazz.isInheritanceFrom(Enum::class.java) -> try {
+        val valueOf = clazz.getDeclaredMethod("valueOf", String::class.java)
+        valueOf.invoke(null, yaml.toString().toUpperCase()) as T
+      } catch (e: Exception) {
+        null
       }
-    } as T
+      yaml is List<*> && clazz.isArray -> parseArray(yaml, clazz) as T
+      else -> when (clazz) {
+        Any::class.java -> yaml
+        Int::class.java -> yaml.toInt()
+        Long::class.java -> yaml.toLong()
+        Float::class.java -> yaml.toFloat()
+        Double::class.java -> yaml.toDouble()
+        Boolean::class.java -> yaml.toBoolean()
+
+        getClazz<Int>() -> yaml.toInt()
+        getClazz<Long>() -> yaml.toLong()
+        getClazz<Float>() -> yaml.toFloat()
+        getClazz<Double>() -> yaml.toDouble()
+        getClazz<Boolean>() -> yaml.toBoolean()
+        String::class.java -> yaml.toString()
+
+        else -> {
+          if (yaml !is Map<*, *>) return null
+          val instance = try {
+            clazz.newInstance()
+          } catch (e: Exception) {
+            unsafe.allocateInstance(clazz)
+          }
+          val fields = clazz.declaredFields
+          fields.forEach {
+            if ((it.modifiers and (Modifier.STATIC or Modifier.TRANSIENT)) != 0) return@forEach
+            try {
+              val parse = parseField(yaml[it.name] ?: return@forEach, it) ?: return@forEach
+              it.isAccessible = true
+              it.set(instance, parse)
+            } catch (e: Exception) {
+            }
+          }
+          instance
+        }
+      } as T
+    }
   }
 
   private fun parseField(yaml: Any, field: Field): Any? {
     val clazz = field.type
     @Suppress("UNCHECKED_CAST")
-    return if (yaml is List<*>) {
-      when {
-        clazz.isAssignableFrom(List::class.java) -> {
-          val type = field.actualTypeArguments
-          if (type == Any::class.java) {
-            yaml
-          } else {
-            val list = try {
-              clazz.newInstance() as MutableList<Any>
-            } catch (e: Exception) {
-              try {
-                unsafe.allocateInstance(clazz) as MutableList<Any>
+    return when (yaml) {
+      is List<*> -> {
+        when {
+          clazz.isAssignableFrom(List::class.java) -> {
+            val type = field.actualTypeArguments
+            if (type == Any::class.java) {
+              yaml
+            } else {
+              val list = try {
+                clazz.newInstance() as MutableList<Any>
               } catch (e: Exception) {
-                ArrayList<Any>()
+                try {
+                  unsafe.allocateInstance(clazz) as MutableList<Any>
+                } catch (e: Exception) {
+                  ArrayList<Any>()
+                }
               }
+              yaml.forEach {
+                list.add(parse(it ?: return@forEach, type) ?: return@forEach)
+              }
+              list
             }
-            yaml.forEach {
-              list.add(parse(it ?: return@forEach, type) ?: return@forEach)
-            }
-            list
           }
+          clazz.isAssignableFrom(Set::class.java) -> {
+            val type = field.actualTypeArguments
+            if (type == Any::class.java) {
+              yaml
+            } else {
+              val set: MutableSet<Any> = try {
+                clazz.newInstance() as MutableSet<Any>
+              } catch (e: Exception) {
+                try {
+                  unsafe.allocateInstance(clazz) as MutableSet<Any>
+                } catch (e: Exception) {
+                  HashSet()
+                }
+              }
+              yaml.forEach {
+                set.add(parse(it ?: return@forEach, type) ?: return@forEach)
+              }
+              set
+            }
+          }
+          clazz.isArray -> parseArray(yaml, clazz)
+          else -> null
         }
-        clazz.isArray -> parseArray(yaml, clazz)
-        else -> null
       }
-    } else {
-      parse(yaml, clazz)
+      else -> {
+        parse(yaml, clazz)
+      }
     }
   }
 
diff --git a/utils/mail/src/main/kotlin/cn/tursom/mail/EmailData.kt b/utils/mail/src/main/kotlin/cn/tursom/mail/EmailData.kt
index ab2a0a8..9230afa 100644
--- a/utils/mail/src/main/kotlin/cn/tursom/mail/EmailData.kt
+++ b/utils/mail/src/main/kotlin/cn/tursom/mail/EmailData.kt
@@ -8,7 +8,6 @@ import javax.activation.DataHandler
 import javax.activation.FileDataSource
 import javax.mail.Address
 import javax.mail.Session
-import javax.mail.event.TransportEvent
 import javax.mail.event.TransportListener
 import javax.mail.internet.InternetAddress
 import javax.mail.internet.MimeBodyPart
diff --git a/utils/mail/src/main/kotlin/cn/tursom/mail/read.kt b/utils/mail/src/main/kotlin/cn/tursom/mail/read.kt
new file mode 100644
index 0000000..f7a5841
--- /dev/null
+++ b/utils/mail/src/main/kotlin/cn/tursom/mail/read.kt
@@ -0,0 +1,120 @@
+package cn.tursom.mail
+
+import java.nio.charset.Charset
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.ScheduledThreadPoolExecutor
+import java.util.concurrent.ThreadFactory
+import java.util.concurrent.TimeUnit
+import javax.mail.*
+import javax.mail.event.MessageCountListener
+import javax.mail.internet.InternetAddress
+
+private val threadPool by lazy {
+  ScheduledThreadPoolExecutor(
+    1,
+    ThreadFactory { Thread(it, "MailLoop") })
+}
+
+fun addMailListener(
+  folder: Folder,
+  freq: Long, timeUnit: TimeUnit,
+  listener: MessageCountListener,
+  newFolder: () -> Folder? = { null }
+): ScheduledFuture<*> {
+  folder.addMessageCountListener(listener)
+  var mailFOlder = folder
+  return threadPool.scheduleAtFixedRate({
+    try {
+      mailFOlder.messageCount
+    } catch (e: FolderClosedException) {
+      mailFOlder = newFolder() ?: throw e
+      mailFOlder.addMessageCountListener(listener)
+    } catch (e: Exception) {
+      e.printStackTrace()
+      throw e
+    }
+  }, 0, freq, timeUnit)
+}
+
+fun addMailListener(
+  newFolder: () -> Folder,
+  freq: Long, timeUnit: TimeUnit,
+  listener: MessageCountListener
+): ScheduledFuture<*> = addMailListener(newFolder(), freq, timeUnit, listener, newFolder)
+
+fun getStore(host: String, port: Int, account: String, password: String): Store {
+  val props = System.getProperties()
+  props["mail.imap.host"] = host
+  props["mail.imap.port"] = port
+  props["mail.imap.auth"] = "true"
+  props["mail.imap.ssl.enable"] = "true"
+  props["mail.imap.socketFactory.port"] = port
+  props["mail.imap.socketFactory.fallback"] = "false"
+  props["mail.imap.socketFactory.class"] = "javax.net.ssl.SSLSocketFactory"
+  val session = Session.getDefaultInstance(props)
+  val store = session.getStore("imap")
+  store.connect(account, password)
+  return store
+}
+
+fun getFolder(host: String, port: Int, account: String, password: String): Pair<Store, Folder> {
+  val store: Store = getStore(host, port, account, password)
+  val folder: Folder = store.getFolder("INBOX")
+  folder.open(Folder.READ_ONLY)
+  return store to folder
+}
+
+fun forEachMail(host: String, port: Int, account: String, password: String, onMsg: (msg: Message) -> Unit) {
+  val (store, folder) = getFolder(host, port, account, password)
+  // 全部邮件数
+  val messages: Array<Message> = folder.messages
+  messages.forEach(onMsg)
+  folder.close(true)
+  store.close()
+}
+
+fun getText(message: Message): String? = when (val content: Any = message.content) {
+  is String -> content
+  is Multipart -> {
+    val sb = StringBuilder()
+    repeat(content.count) {
+      val part: Part = content.getBodyPart(it)
+      if (part.contentType.contains("text/plain")) {
+        var charsetStart = part.contentType.indexOf("charset=", 0, true)
+        if (charsetStart != -1) charsetStart += "charset=".length
+        var charsetEnd = part.contentType.indexOf(';', charsetStart)
+        if (charsetEnd == -1) {
+          charsetEnd = part.contentType.length
+        }
+        val msg = part.inputStream.bufferedReader(
+          if (charsetStart == -1) {
+            Charsets.UTF_8
+          } else {
+            Charset.forName(part.contentType.substring(charsetStart, charsetEnd))
+          }
+        ).readText()
+        sb.append(msg)
+      }
+    }
+    if (sb.isEmpty()) {
+      null
+    } else {
+      sb.toString()
+    }
+  }
+  else -> null
+}
+
+fun forEachUnreadMail(host: String, port: Int, account: String, password: String, onMsg: (from: InternetAddress, msg: String) -> Boolean) {
+  forEachMail(host, port, account, password) { message ->
+    // 跳过已读邮件
+    if (message.flags.contains(Flags.Flag.SEEN)) return@forEachMail
+    val msg = getText(message)
+    val read = when (val text = getText(message)) {
+      null -> false
+      else -> onMsg(message.from[0] as InternetAddress, text)
+    }
+    //未读邮件标记为已读
+    message.setFlag(Flags.Flag.SEEN, read)
+  }
+}
diff --git a/utils/yaml/src/main/kotlin/cn/tursom/yaml/Yaml.kt b/utils/yaml/src/main/kotlin/cn/tursom/yaml/Yaml.kt
index 22952da..23f7e8e 100644
--- a/utils/yaml/src/main/kotlin/cn/tursom/yaml/Yaml.kt
+++ b/utils/yaml/src/main/kotlin/cn/tursom/yaml/Yaml.kt
@@ -16,57 +16,78 @@ object Yaml {
     return stringBuilder.toString()
   }
 
-  private fun toYaml(obj: Any, stringBuilder: StringBuilder, indentation: String) {
+  private fun toYaml(obj: Any, stringBuilder: StringBuilder, indentation: String, inCollection: Boolean = false) {
     when (obj) {
-      is Byte -> stringBuilder.append("$obj")
-      is Char -> stringBuilder.append("$obj")
-      is Short -> stringBuilder.append("$obj")
-      is Int -> stringBuilder.append("$obj")
-      is Long -> stringBuilder.append("$obj")
-      is Float -> stringBuilder.append("$obj")
-      is Double -> stringBuilder.append("$obj")
-      is String -> stringBuilder.append("$obj")
-      is Collection<*> -> obj.forEach {
-        it ?: return@forEach
-        stringBuilder.append("$indentation- ")
-        toYaml(it, stringBuilder, "$indentation  ")
-        if (!stringBuilder.endsWith('\n')) {
-          stringBuilder.append("\n")
+      is Byte -> stringBuilder.append(obj)
+      is Char -> stringBuilder.append(obj)
+      is Short -> stringBuilder.append(obj)
+      is Int -> stringBuilder.append(obj)
+      is Long -> stringBuilder.append(obj)
+      is Float -> stringBuilder.append(obj)
+      is Double -> stringBuilder.append(obj)
+      is String -> stringBuilder.append(obj)
+      is Map<*, *> -> {
+        var first = true
+        obj.forEach { (any, u) ->
+          if (inCollection && first) {
+            stringBuilder.append("${any ?: return@forEach}: ")
+            first = false
+          } else {
+            stringBuilder.append("$indentation${any ?: return@forEach}: ")
+          }
+          toYaml(u ?: return@forEach, stringBuilder, "$indentation  ")
+          if (!stringBuilder.endsWith('\n')) {
+            stringBuilder.append("\n")
+          }
         }
       }
-      is Map<*, *> -> obj.forEach { (any, u) ->
-        stringBuilder.append("$indentation${any ?: return@forEach}: ")
-        toYaml(u ?: return@forEach, stringBuilder, "$indentation  ")
-        if (!stringBuilder.endsWith('\n')) {
-          stringBuilder.append("\n")
+      is Collection<*> -> if (obj.isEmpty()) {
+        stringBuilder.append("[]")
+      } else {
+        var appended = 0
+        obj.forEach {
+          it ?: return@forEach
+          stringBuilder.append("${if (appended == 0) '\n' else ""}$indentation- ")
+          appended++
+          toYaml(it, stringBuilder, "$indentation  ", true)
+          if (!stringBuilder.endsWith('\n')) {
+            stringBuilder.append("\n")
+          }
         }
       }
       else -> {
+        var first = true
+        fun getIndentation() = if (inCollection && first) {
+          first = false
+          ""
+        } else {
+          indentation
+        }
         obj.javaClass.declaredFields.forEach {
           if ((it.modifiers and (Modifier.STATIC or Modifier.TRANSIENT)) != 0) return@forEach
           it.isAccessible = true
           val value = it.get(obj)
           when (it.type) {
-            Byte::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
-            Char::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
-            Short::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
-            Int::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
-            Long::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
-            Float::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
-            Double::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
+            Byte::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            Char::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            Short::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            Int::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            Long::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            Float::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            Double::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
 
-            getClazz<Byte>() -> stringBuilder.append("$indentation${it.name}: $value\n")
-            getClazz<Char>() -> stringBuilder.append("$indentation${it.name}: $value\n")
-            getClazz<Short>() -> stringBuilder.append("$indentation${it.name}: $value\n")
-            getClazz<Int>() -> stringBuilder.append("$indentation${it.name}: $value\n")
-            getClazz<Long>() -> stringBuilder.append("$indentation${it.name}: $value\n")
-            getClazz<Float>() -> stringBuilder.append("$indentation${it.name}: $value\n")
-            getClazz<Double>() -> stringBuilder.append("$indentation${it.name}: $value\n")
-            String::class.java -> stringBuilder.append("$indentation${it.name}: $value\n")
+            getClazz<Byte>() -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            getClazz<Char>() -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            getClazz<Short>() -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            getClazz<Int>() -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            getClazz<Long>() -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            getClazz<Float>() -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            getClazz<Double>() -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
+            String::class.java -> stringBuilder.append("${getIndentation()}${it.name}: $value\n")
 
             else -> {
-              stringBuilder.append("$indentation${it.name}:\n")
-              toYaml(value, stringBuilder, "$indentation  ")
+              stringBuilder.append("${getIndentation()}${it.name}: ")
+              toYaml(value, stringBuilder, "${getIndentation()}  ")
             }
           }
         }
@@ -81,7 +102,6 @@ object Yaml {
   inline fun <reified T> parseResource(classLoader: ClassLoader, path: String) = parseResource(classLoader, path, T::class.java)
 
   fun <T> parseResource(path: String, clazz: Class<T>) = parseResource(this.javaClass.classLoader, path, clazz)
-  fun <T> parseResource(classLoader: ClassLoader, path: String, clazz: Class<T>) = Parser.parse(yaml.load<Any>(classLoader.getResourceAsStream(path)), clazz)
-  fun <T> parse(yaml: String, clazz: Class<T>) = Parser.parse(this.yaml.load<Any>(yaml), clazz)
-
+  fun <T> parseResource(classLoader: ClassLoader, path: String, clazz: Class<T>) = Parser.parse(yaml.load(classLoader.getResourceAsStream(path)), clazz)
+  fun <T> parse(yaml: String, clazz: Class<T>) = Parser.parse(this.yaml.load(yaml), clazz)
 }
\ No newline at end of file