feat(mail-service): initialize Mail-Service and integrate online nomination workflow

- Created `MailServiceApplication` with Spring Boot setup.
- Added `MailPollingService` for IMAP polling, `TurnierNr` extraction, and auto-reply functionality.
- Implemented structured email sending for online nominations via `OnlineNennungFormular`.
- Updated frontend with `Erfolgsscreen` for nomination confirmation and fallback handling.
- Added build configurations for Mail-Service and frontend nomination module.
- Documented phase-based roadmap for Online-Nennung and Mail-Service rollout.
This commit is contained in:
2026-04-14 14:59:11 +02:00
parent 5f87eed86a
commit adfa97978e
9 changed files with 497 additions and 5 deletions
@@ -0,0 +1,36 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinSpring)
}
springBoot {
mainClass.set("at.mocode.mail.service.MailServiceApplicationKt")
}
dependencies {
// Interne Module
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreUtils)
implementation(projects.core.coreDomain)
// Spring Boot Starters
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator)
implementation(libs.spring.boot.starter.mail)
implementation(libs.jackson.module.kotlin)
implementation(libs.spring.cloud.starter.consul.discovery)
implementation(libs.micrometer.tracing.bridge.brave)
implementation(libs.zipkin.reporter.brave)
implementation(libs.zipkin.sender.okhttp3)
// Testing
testImplementation(projects.platform.platformTesting)
testImplementation(libs.spring.boot.starter.test)
}
tasks.test {
useJUnitPlatform()
}
@@ -0,0 +1,114 @@
package at.mocode.mail.service
import jakarta.mail.Flags
import jakarta.mail.Folder
import jakarta.mail.Session
import jakarta.mail.internet.InternetAddress
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
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.*
@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 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
}
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 session = Session.getInstance(props)
val store = session.getStore("imaps")
store.connect(imapHost, username, password)
val inbox = store.getFolder("INBOX")
inbox.open(Folder.READ_WRITE)
// Nur ungelesene Nachrichten
val messages = inbox.getMessages()
logger.info("Gefundene Nachrichten in INBOX: ${messages.size}")
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 ?: ""
logger.info("Neue Mail empfangen von: ${message.from?.firstOrNull()} an: $toAddress")
// Turnier-Nr extrahieren: meldestelle-26128@mo-code.at
val turnierNr = extractTurnierNr(toAddress)
if (turnierNr != null) {
logger.info("Nennung für Turnier $turnierNr erkannt.")
// TODO: Payload parsen und in DB speichern (Tenant-Routing)
// Auto-Reply senden
sendAutoReply(message.from?.firstOrNull()?.toString() ?: "", turnierNr)
// Mail als gelesen markieren
message.setFlag(Flags.Flag.SEEN, true)
} else {
logger.warn("Keine Turnier-Nr in Adresse $toAddress gefunden. Mail wird ignoriert.")
}
}
}
inbox.close(false)
store.close()
} catch (e: Exception) {
logger.error("Fehler beim Mail-Polling: ${e.message}", e)
}
}
private fun extractTurnierNr(address: String): String? {
val regex = Regex("meldestelle-(\\d+)@.*")
val match = regex.find(address)
return match?.groupValues?.get(1)
}
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.
Ihre Nennung ist erfolgreich in unserem System eingegangen und wird nun von der Meldestelle geprüft.
Sobald die Nennung final verarbeitet wurde, erhalten Sie eine weitere Bestätigung.
Mit freundlichen Grüßen,
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}")
}
}
}
@@ -0,0 +1,11 @@
package at.mocode.mail.service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class MailServiceApplication
fun main(args: Array<String>) {
runApplication<MailServiceApplication>(*args)
}
@@ -0,0 +1,32 @@
spring:
application:
name: mail-service
mail:
host: ${MAIL_HOST:imap.world4you.com}
port: ${MAIL_PORT:993}
username: ${MAIL_USERNAME:online-nennen@mo-code.at}
password: ${MAIL_PASSWORD:}
properties:
mail:
store:
protocol: imaps
imaps:
host: ${MAIL_HOST:imap.world4you.com}
port: ${MAIL_PORT:993}
ssl:
enable: true
smtp:
auth: true
starttls:
enable: true
host-smtp: ${SMTP_HOST:smtp.world4you.com}
port-smtp: ${SMTP_PORT:587}
server:
port: 8085
management:
endpoints:
web:
exposure:
include: "health,info,prometheus"