Skip to content

Commit

Permalink
feat: auto mail
Browse files Browse the repository at this point in the history
  • Loading branch information
cssxsh committed Dec 19, 2022
1 parent 53339bf commit 2a7037f
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ repositories {

dependencies {
api("com.cronutils:cron-utils:9.2.0")
api("jakarta.mail:jakarta.mail-api:2.1.0")
implementation("org.eclipse.angus:angus-mail:1.0.0")
compileOnly("javax.validation:validation-api:2.0.1.Final")
testImplementation(kotlin("test"))
//
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/xyz/cssxsh/mirai/admin/MiraiAdminPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal object MiraiAdminPlugin : KotlinPlugin(
AdminSetting.reload()
AdminBlackListData.reload()
AdminAutoQuitConfig.reload()
AdminMailConfig.reload()

if (AdminSetting.owner != AdminSetting.OWNER_DEFAULT) {
logger.info { "机器人所有者 ${AdminSetting.owner}" }
Expand Down
75 changes: 75 additions & 0 deletions src/main/kotlin/xyz/cssxsh/mirai/admin/MiraiAdministrator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import net.mamoe.mirai.message.data.MessageSource.Key.quote
import net.mamoe.mirai.utils.*
import xyz.cssxsh.mirai.admin.command.*
import xyz.cssxsh.mirai.admin.data.*
import xyz.cssxsh.mirai.admin.mail.*
import xyz.cssxsh.mirai.spi.*
import kotlin.collections.*
import kotlin.coroutines.*
import kotlin.io.path.*

/**
* 事件监听及定时器
Expand Down Expand Up @@ -456,5 +458,78 @@ public object MiraiAdministrator : SimpleListenerHost() {
}
}

@EventHandler
internal fun BotOfflineEvent.handle() {
if (AdminMailConfig.notify.not()) return
if (AdminMailConfig.close.not() && this is BotOfflineEvent.Active) return
val session = buildMailSession {
AdminMailConfig.properties.inputStream().use {
load(it)
}
}
val offline = this

launch {
val mail = buildMailContent(session) {
to = AdminMailConfig.offline.ifEmpty { "${AdminSetting.owner}@qq.com" }
title = "机器人下线通知 $bot"
text {
@OptIn(MiraiInternalApi::class)
when (offline) {
is BotOfflineEvent.Active -> {
append("主动离线")
}
is BotOfflineEvent.Dropped -> {
append("因网络问题而掉线")
if (offline.cause != null) {
append('\n')
append("cause:\n")
append(offline.cause!!.stackTraceToString())
}
}
is BotOfflineEvent.Force -> {
append("被挤下线.")
}
is BotOfflineEvent.MsfOffline -> {
append("被服务器断开.")
if (offline.cause != null) {
append('\n')
append("cause:\n")
append(offline.cause!!.stackTraceToString())
}
}
is BotOfflineEvent.RequireReconnect -> {
append("服务器主动要求更换另一个服务器.")
if (offline.cause != null) {
append('\n')
append("cause:\n")
append(offline.cause!!.stackTraceToString())
}
}
}
}
file("console.log") {
val logs = java.io.File("logs")
logs.listFiles()?.maxByOrNull { it.lastModified() }

}
file("network.log") {
val logs = java.io.File("bots/${bot.id}/logs")
logs.listFiles()?.maxByOrNull { it.lastModified() }
}
}

val oc = Thread.currentThread().contextClassLoader
try {
Thread.currentThread().contextClassLoader = AdminMailConfig::class.java.classLoader
jakarta.mail.Transport.send(mail)
} catch (cause: jakarta.mail.MessagingException) {
logger.error({ "邮件发送失败" }, cause)
} finally {
Thread.currentThread().contextClassLoader = oc
}
}
}

// endregion
}
52 changes: 52 additions & 0 deletions src/main/kotlin/xyz/cssxsh/mirai/admin/command/AdminSendCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.*
import xyz.cssxsh.mirai.admin.*
import xyz.cssxsh.mirai.admin.data.*
import xyz.cssxsh.mirai.admin.mail.*
import kotlin.io.path.*

/**
* 发送相关指令
Expand Down Expand Up @@ -112,4 +115,53 @@ public object AdminSendCommand : CompositeCommand(

sendMessage(message)
}

/**
* 备份日志到邮箱
* @param addresses 接收的邮箱
*/
@SubCommand
@Description("备份日志到邮箱")
public suspend fun CommandSender.log(vararg addresses: String) {
val session = buildMailSession {
AdminMailConfig.properties.inputStream().use {
load(it)
}
}

val mail = buildMailContent(session) {
to = addresses.joinToString().ifEmpty { AdminMailConfig.log }
title = "日志备份"
text {
val plugins = java.io.File("plugins")
append("plugins: \n")
for (file in plugins.listFiles().orEmpty()) {
append(file.name).append(" ").append(file.length().div(1024)).append("KB").append('\n')
}
val libs = java.io.File("libs")
append("libs: \n")
for (file in libs.listFiles().orEmpty()) {
append(file.name).append(" ").append(file.length().div(1024)).append("KB").append('\n')
}
}
file("console.log") {
val logs = java.io.File("logs")
logs.listFiles()?.maxByOrNull { it.lastModified() }
}
file("network.log") {
val logs = java.io.File("bots/${bot?.id}/logs")
logs.listFiles()?.maxByOrNull { it.lastModified() }
}
}

val oc = Thread.currentThread().contextClassLoader
try {
Thread.currentThread().contextClassLoader = AdminMailConfig::class.java.classLoader
jakarta.mail.Transport.send(mail)
} catch (cause: jakarta.mail.MessagingException) {
sendMessage("邮件发送失败, cause: ${cause.message}")
} finally {
Thread.currentThread().contextClassLoader = oc
}
}
}
58 changes: 58 additions & 0 deletions src/main/kotlin/xyz/cssxsh/mirai/admin/data/AdminMailConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package xyz.cssxsh.mirai.admin.data

import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.plugin.*
import net.mamoe.mirai.console.plugin.jvm.*
import net.mamoe.mirai.console.util.*
import net.mamoe.mirai.utils.*
import java.io.*
import kotlin.io.path.*

@PublishedApi
internal object AdminMailConfig : ReadOnlyPluginConfig("AdminMailConfig") {

@ValueName("offline_notify")
@ValueDescription("机器人下线时,发送邮件")
val notify: Boolean by value(true)

@ValueName("close_notify")
@ValueDescription("机器人正常关闭时,也发送邮件")
val close: Boolean by value(false)

@ValueName("bot_offline")
@ValueDescription("机器人下线时,接收邮件的地址")
val offline: String by value("")

@ValueName("log_backup")
@ValueDescription("备份日志时,接收邮件的地址")
val log: String by value("")

var properties = Path("admin.mail.properties")
private set

@OptIn(ConsoleExperimentalApi::class)
override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) {
if (owner is JvmPlugin) {
properties = owner.resolveConfigPath("admin.mail.properties")
if (properties.notExists()) {
properties.writeText(
"""
mail.host=smtp.example.com
mail.auth=true
mail.user=xxx
mail.password=****
[email protected]
mail.store.protocol=smtp
mail.transport.protocol=smtp
# smtp
mail.smtp.starttls.enable=true
mail.smtp.auth=true
mail.smtp.timeout=15000
""".trimIndent()
)
owner.logger.info { "邮件配置文件已生成,请修改内容以生效 $properties" }
}
}
}
}
133 changes: 133 additions & 0 deletions src/main/kotlin/xyz/cssxsh/mirai/admin/mail/build.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package xyz.cssxsh.mirai.admin.mail

import jakarta.activation.*
import jakarta.mail.*
import jakarta.mail.internet.*
import java.io.*
import java.nio.file.*
import java.util.*

/**
* [Environment Properties](https://jakarta.ee/specifications/mail/2.1/jakarta-mail-spec-2.1.html#a823)
*/
public fun buildMailSession(block: Properties.() -> Unit): Session {
val props = Properties(System.getProperties())
val oc = Thread.currentThread().contextClassLoader
try {
block.invoke(props)
} finally {
Thread.currentThread().contextClassLoader = oc
}

if (props.getProperty("mail.smtp.localhost") == null) {
props.setProperty("mail.smtp.localhost", props.getProperty("mail.host"))
}

val auth = object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
val user = props["mail.${requestingProtocol}.user"]?.toString()
?: props["mail.user"]?.toString()
?: System.getenv("MAIL_USER")
?: throw NoSuchElementException("mail.user")
val password = props["mail.${requestingProtocol}.password"]?.toString()
?: props["mail.password"]?.toString()
?: System.getenv("MAIL_PASSWORD")
?: throw NoSuchElementException("mail.password")
return PasswordAuthentication(user, password)
}
}

return try {
Thread.currentThread().contextClassLoader = block::class.java.classLoader
Session.getDefaultInstance(props, auth)
} finally {
Thread.currentThread().contextClassLoader = oc
}
}

/**
* 构建邮件
* @see MailContentBuilder
*/
public fun buildMailContent(session: Session, block: MailContentBuilder.(MimeMessage) -> Unit): MimeMessage {
val message = MimeMessage(session)
val builder: MailContentBuilder

val oc = Thread.currentThread().contextClassLoader
try {
Thread.currentThread().contextClassLoader = block::class.java.classLoader
builder = MailContentBuilder(session)
block.invoke(builder, message)

message.setFrom(builder.from)

message.setRecipients(MimeMessage.RecipientType.TO, builder.to)

if (builder.title.isEmpty()) throw throw IllegalArgumentException("title is empty")
message.setSubject(builder.title, "UTF-8")

if (builder.content.count == 0) throw throw IllegalArgumentException("content is empty")
message.setContent(builder.content)

} finally {
Thread.currentThread().contextClassLoader = oc
}

return message
}

@DslMarker
public annotation class MailDsl

public class MailContentBuilder(session: Session) {
@MailDsl
public var from: String? = session.getProperty("mail.from")

@MailDsl
public var to: String? = null

@MailDsl
public var title: String = ""

@MailDsl
public var content: MimeMultipart = MimeMultipart()

@MailDsl
public fun text(type: String = "plain", builderAction: StringBuilder.() -> Unit) {
val part = MimeBodyPart()
part.setText(StringBuilder().apply(builderAction).toString(), "UTF-8", type)
content.addBodyPart(part)
}

@MailDsl
public fun file(filename: String? = null, builderAction: () -> Any?) {
val part = MimeBodyPart()
when (val target = builderAction()) {
is File -> {
part.dataHandler = DataHandler(FileDataSource(target))
part.fileName = filename ?: target.name
}
is Path -> {
val file = target.toFile()
part.dataHandler = DataHandler(FileDataSource(file))
part.fileName = filename ?: file.name
}
is DataSource -> {
part.dataHandler = DataHandler(target)
part.fileName = filename ?: target.name ?: "data.bin"
}
is DataHandler -> {
part.dataHandler = target
part.fileName = filename ?: target.name ?: "data.bin"
}
is String -> {
val file = File(target)
part.dataHandler = DataHandler(FileDataSource(file))
part.fileName = filename ?: file.name
}
Unit, null -> return
else -> throw IllegalArgumentException("file")
}
content.addBodyPart(part)
}
}

0 comments on commit 2a7037f

Please sign in to comment.