feat(mail-service): introduce persistence and REST support for Nennungen
- Added `NennungRepository` with methods for saving, updating status, and retrieving entries. - Created `NennungController` to expose REST endpoints for Nennungen. - Defined `NennungTable` schema with relevant fields and indices. - Extended `MailPollingService` to parse incoming emails into `NennungEntity` and persist them. - Updated `build.gradle.kts` with database dependencies and H2 configuration for local dev. - Refined frontend layout in `OnlineNennungFormular` for improved usability and responsiveness.
This commit is contained in:
@@ -11,6 +11,7 @@ springBoot {
|
||||
|
||||
dependencies {
|
||||
// Interne Module
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
@@ -20,7 +21,20 @@ dependencies {
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
implementation(libs.spring.boot.starter.actuator)
|
||||
implementation(libs.spring.boot.starter.mail)
|
||||
implementation(libs.spring.boot.starter.jdbc)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
implementation(libs.jackson.datatype.jsr310)
|
||||
|
||||
// Database & Exposed
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.java.time)
|
||||
implementation(libs.exposed.json)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
implementation(libs.h2.driver)
|
||||
implementation(libs.postgresql.driver)
|
||||
implementation(libs.hikari.cp)
|
||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||
implementation(libs.micrometer.tracing.bridge.brave)
|
||||
implementation(libs.zipkin.reporter.brave)
|
||||
|
||||
+116
-65
@@ -1,99 +1,150 @@
|
||||
package at.mocode.mail.service
|
||||
|
||||
import at.mocode.mail.service.persistence.NennungEntity
|
||||
import at.mocode.mail.service.persistence.NennungRepository
|
||||
import at.mocode.mail.service.persistence.NennungTable
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import jakarta.mail.Flags
|
||||
import jakarta.mail.Folder
|
||||
import jakarta.mail.Session
|
||||
import jakarta.mail.internet.InternetAddress
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.mail.SimpleMailMessage
|
||||
import org.springframework.mail.javamail.JavaMailSender
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.*
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Service
|
||||
@EnableScheduling
|
||||
class MailPollingService(
|
||||
private val mailSender: JavaMailSender,
|
||||
@Value("\${spring.mail.host}") private val imapHost: String,
|
||||
@Value("\${spring.mail.port}") private val imapPort: Int,
|
||||
@Value("\${spring.mail.username}") private val username: String,
|
||||
@Value("\${spring.mail.password}") private val password: String
|
||||
private val mailSender: JavaMailSender,
|
||||
private val nennungRepository: NennungRepository,
|
||||
private val objectMapper: ObjectMapper,
|
||||
@Value("\${spring.mail.host}") private val imapHost: String,
|
||||
@Value("\${spring.mail.port}") private val imapPort: Int,
|
||||
@Value("\${spring.mail.username}") private val username: String,
|
||||
@Value("\${spring.mail.password}") private val password: String
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(MailPollingService::class.java)
|
||||
private val logger = LoggerFactory.getLogger(MailPollingService::class.java)
|
||||
|
||||
@Scheduled(fixedDelay = 60000) // Alle 60 Sekunden pollen
|
||||
fun pollMails() {
|
||||
if (password.isBlank()) {
|
||||
logger.warn("Mail-Passwort nicht gesetzt. Polling übersprungen.")
|
||||
return
|
||||
}
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
@Transactional
|
||||
fun initSchema() {
|
||||
transaction {
|
||||
SchemaUtils.create(NennungTable)
|
||||
}
|
||||
logger.info("Datenbankschema für Mail-Service initialisiert.")
|
||||
}
|
||||
|
||||
try {
|
||||
val props = Properties()
|
||||
props["mail.store.protocol"] = "imaps"
|
||||
props["mail.imaps.host"] = imapHost
|
||||
props["mail.imaps.port"] = imapPort.toString()
|
||||
props["mail.imaps.ssl.enable"] = "true"
|
||||
@Scheduled(fixedDelay = 60000) // Alle 60 Sekunden pollen
|
||||
fun pollMails() {
|
||||
if (password.isBlank()) {
|
||||
logger.warn("Mail-Passwort nicht gesetzt. Polling übersprungen.")
|
||||
return
|
||||
}
|
||||
|
||||
val session = Session.getInstance(props)
|
||||
val store = session.getStore("imaps")
|
||||
store.connect(imapHost, username, password)
|
||||
try {
|
||||
val props = Properties()
|
||||
props["mail.store.protocol"] = "imaps"
|
||||
props["mail.imaps.host"] = imapHost
|
||||
props["mail.imaps.port"] = imapPort.toString()
|
||||
props["mail.imaps.ssl.enable"] = "true"
|
||||
|
||||
val inbox = store.getFolder("INBOX")
|
||||
inbox.open(Folder.READ_WRITE)
|
||||
val session = Session.getInstance(props)
|
||||
val store = session.getStore("imaps")
|
||||
store.connect(imapHost, username, password)
|
||||
|
||||
// Nur ungelesene Nachrichten
|
||||
val messages = inbox.getMessages()
|
||||
logger.info("Gefundene Nachrichten in INBOX: ${messages.size}")
|
||||
val inbox = store.getFolder("INBOX")
|
||||
inbox.open(Folder.READ_WRITE)
|
||||
|
||||
for (message in messages) {
|
||||
if (!message.isSet(Flags.Flag.SEEN)) {
|
||||
val recipients = message.getRecipients(jakarta.mail.Message.RecipientType.TO)
|
||||
val toAddress = (recipients?.firstOrNull() as? InternetAddress)?.address ?: ""
|
||||
// Nur ungelesene Nachrichten
|
||||
val messages = inbox.getMessages()
|
||||
logger.info("Gefundene Nachrichten in INBOX: ${messages.size}")
|
||||
|
||||
logger.info("Neue Mail empfangen von: ${message.from?.firstOrNull()} an: $toAddress")
|
||||
for (message in messages) {
|
||||
if (!message.isSet(Flags.Flag.SEEN)) {
|
||||
val recipients = message.getRecipients(jakarta.mail.Message.RecipientType.TO)
|
||||
val toAddress = (recipients?.firstOrNull() as? InternetAddress)?.address ?: ""
|
||||
|
||||
// Turnier-Nr extrahieren: meldestelle-26128@mo-code.at
|
||||
val turnierNr = extractTurnierNr(toAddress)
|
||||
logger.info("Neue Mail empfangen von: ${message.from?.firstOrNull()} an: $toAddress")
|
||||
|
||||
if (turnierNr != null) {
|
||||
logger.info("Nennung für Turnier $turnierNr erkannt.")
|
||||
// TODO: Payload parsen und in DB speichern (Tenant-Routing)
|
||||
// Turnier-Nr extrahieren: meldestelle-26128@mo-code.at
|
||||
val turnierNr = extractTurnierNr(toAddress)
|
||||
|
||||
// Auto-Reply senden
|
||||
sendAutoReply(message.from?.firstOrNull()?.toString() ?: "", turnierNr)
|
||||
if (turnierNr != null) {
|
||||
logger.info("Nennung für Turnier $turnierNr erkannt.")
|
||||
|
||||
// Mail als gelesen markieren
|
||||
message.setFlag(Flags.Flag.SEEN, true)
|
||||
} else {
|
||||
logger.warn("Keine Turnier-Nr in Adresse $toAddress gefunden. Mail wird ignoriert.")
|
||||
}
|
||||
}
|
||||
try {
|
||||
val content = message.content.toString()
|
||||
|
||||
val entity = NennungEntity(
|
||||
id = Uuid.random(),
|
||||
turnierNr = turnierNr,
|
||||
status = "NEU",
|
||||
vorname = extractValue(content, "Vorname") ?: "Unbekannt",
|
||||
nachname = extractValue(content, "Nachname") ?: "Unbekannt",
|
||||
lizenz = extractValue(content, "Lizenz") ?: "LF",
|
||||
pferdName = extractValue(content, "Pferd") ?: "Unbekannt",
|
||||
pferdAlter = extractValue(content, "Alter") ?: "2020",
|
||||
email = (message.from?.firstOrNull() as? InternetAddress)?.address ?: "unbekannt@test.at",
|
||||
telefon = extractValue(content, "Telefon"),
|
||||
bewerbe = extractValue(content, "Bewerbe") ?: "[]",
|
||||
bemerkungen = extractValue(content, "Bemerkungen")
|
||||
)
|
||||
|
||||
nennungRepository.save(entity)
|
||||
logger.info("Nennung für ${entity.vorname} ${entity.nachname} erfolgreich persistiert.")
|
||||
|
||||
// Auto-Reply senden
|
||||
sendAutoReply(entity.email, turnierNr)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Parsen/Speichern der Nennung: ${e.message}")
|
||||
}
|
||||
|
||||
inbox.close(false)
|
||||
store.close()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Mail-Polling: ${e.message}", e)
|
||||
// Mail als gelesen markieren
|
||||
message.setFlag(Flags.Flag.SEEN, true)
|
||||
} else {
|
||||
logger.warn("Keine Turnier-Nr in Adresse $toAddress gefunden. Mail wird ignoriert.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractTurnierNr(address: String): String? {
|
||||
val regex = Regex("meldestelle-(\\d+)@.*")
|
||||
val match = regex.find(address)
|
||||
return match?.groupValues?.get(1)
|
||||
inbox.close(false)
|
||||
store.close()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Mail-Polling: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendAutoReply(to: String, turnierNr: String) {
|
||||
try {
|
||||
val message = SimpleMailMessage()
|
||||
message.from = username
|
||||
message.setTo(to)
|
||||
message.subject = "Eingangsbestätigung: Ihre Nennung für Turnier $turnierNr"
|
||||
message.text = """
|
||||
private fun extractTurnierNr(address: String): String? {
|
||||
val regex = Regex("meldestelle-(\\d+)@.*")
|
||||
val match = regex.find(address)
|
||||
return match?.groupValues?.get(1)
|
||||
}
|
||||
|
||||
private fun extractValue(content: String, key: String): String? {
|
||||
val regex = Regex("$key:\\s*(.*)")
|
||||
return regex.find(content)?.groupValues?.get(1)?.trim()
|
||||
}
|
||||
|
||||
private fun sendAutoReply(to: String, turnierNr: String) {
|
||||
try {
|
||||
val message = SimpleMailMessage()
|
||||
message.from = username
|
||||
message.setTo(to)
|
||||
message.subject = "Eingangsbestätigung: Ihre Nennung für Turnier $turnierNr"
|
||||
message.text = """
|
||||
Sehr geehrte Damen und Herren,
|
||||
|
||||
vielen Dank für Ihre Online-Nennung für das Turnier $turnierNr.
|
||||
@@ -105,10 +156,10 @@ class MailPollingService(
|
||||
Ihre Turniermeldestelle
|
||||
""".trimIndent()
|
||||
|
||||
mailSender.send(message)
|
||||
logger.info("Auto-Reply an $to für Turnier $turnierNr gesendet.")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Senden des Auto-Replies: ${e.message}")
|
||||
}
|
||||
mailSender.send(message)
|
||||
logger.info("Auto-Reply an $to für Turnier $turnierNr gesendet.")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Senden des Auto-Replies: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@ import org.springframework.boot.runApplication
|
||||
class MailServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<MailServiceApplication>(*args)
|
||||
runApplication<MailServiceApplication>(*args)
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.mail.service
|
||||
|
||||
import at.mocode.mail.service.persistence.NennungEntity
|
||||
import at.mocode.mail.service.persistence.NennungRepository
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/mail/nennungen")
|
||||
class NennungController(
|
||||
private val nennungRepository: NennungRepository
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun getAllNennungen(): List<NennungEntity> {
|
||||
return nennungRepository.findAll()
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/status")
|
||||
fun updateStatus(
|
||||
@PathVariable id: String,
|
||||
@RequestBody newStatus: String
|
||||
) {
|
||||
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.mail.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
data class NennungEntity(
|
||||
val id: Uuid,
|
||||
val turnierNr: String,
|
||||
val status: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferdName: String,
|
||||
val pferdAlter: String,
|
||||
val email: String,
|
||||
val telefon: String?,
|
||||
val bewerbe: String,
|
||||
val bemerkungen: String?
|
||||
)
|
||||
|
||||
@Repository
|
||||
@Transactional
|
||||
class NennungRepository {
|
||||
|
||||
fun save(nennung: NennungEntity) {
|
||||
transaction {
|
||||
NennungTable.insert {
|
||||
it[id] = nennung.id
|
||||
it[turnierNr] = nennung.turnierNr
|
||||
it[status] = nennung.status
|
||||
it[vorname] = nennung.vorname
|
||||
it[nachname] = nennung.nachname
|
||||
it[lizenz] = nennung.lizenz
|
||||
it[pferdName] = nennung.pferdName
|
||||
it[pferdAlter] = nennung.pferdAlter
|
||||
it[email] = nennung.email
|
||||
it[telefon] = nennung.telefon
|
||||
it[bewerbe] = nennung.bewerbe
|
||||
it[bemerkungen] = nennung.bemerkungen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStatus(id: Uuid, newStatus: String) {
|
||||
transaction {
|
||||
NennungTable.update({ NennungTable.id eq id }) {
|
||||
it[status] = newStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findAll(): List<NennungEntity> {
|
||||
return transaction {
|
||||
NennungTable.selectAll().map {
|
||||
NennungEntity(
|
||||
id = it[NennungTable.id],
|
||||
turnierNr = it[NennungTable.turnierNr],
|
||||
status = it[NennungTable.status],
|
||||
vorname = it[NennungTable.vorname],
|
||||
nachname = it[NennungTable.nachname],
|
||||
lizenz = it[NennungTable.lizenz],
|
||||
pferdName = it[NennungTable.pferdName],
|
||||
pferdAlter = it[NennungTable.pferdAlter],
|
||||
email = it[NennungTable.email],
|
||||
telefon = it[NennungTable.telefon],
|
||||
bewerbe = it[NennungTable.bewerbe],
|
||||
bemerkungen = it[NennungTable.bemerkungen]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.mail.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.javatime.CurrentTimestamp
|
||||
import org.jetbrains.exposed.v1.javatime.timestamp
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
object NennungTable : Table("nennungen") {
|
||||
val id = uuid("id")
|
||||
val turnierNr = varchar("turnier_nr", 20)
|
||||
val status = varchar("status", 20) // NEU, GELESEN, UEBERNOMMEN
|
||||
val eingangsdatum = timestamp("eingangsdatum").defaultExpression(CurrentTimestamp)
|
||||
|
||||
// Reiter Daten
|
||||
val vorname = varchar("vorname", 100)
|
||||
val nachname = varchar("nachname", 100)
|
||||
val lizenz = varchar("lizenz", 50)
|
||||
|
||||
// Pferd Daten
|
||||
val pferdName = varchar("pferd_name", 100)
|
||||
val pferdAlter = varchar("pferd_alter", 10)
|
||||
|
||||
// Kontakt
|
||||
val email = varchar("email", 150)
|
||||
val telefon = varchar("telefon", 50).nullable()
|
||||
|
||||
// Payload (Bewerbe & Bemerkungen)
|
||||
val bewerbe = text("bewerbe") // Kommagetrennte Liste der Bewerbs-Nummern
|
||||
val bemerkungen = text("bemerkungen").nullable()
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
spring:
|
||||
application:
|
||||
name: mail-service
|
||||
datasource:
|
||||
url: jdbc:h2:mem:maildb;DB_CLOSE_DELAY=-1
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password: ""
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
mail:
|
||||
host: ${MAIL_HOST:imap.world4you.com}
|
||||
port: ${MAIL_PORT:993}
|
||||
|
||||
Reference in New Issue
Block a user