Implemented MailReader that checks all mails of an email account if they have an eInvoice as attachment
This commit is contained in:
parent
bb48356011
commit
42f4d09ff3
|
@ -10,6 +10,8 @@ kotlin {
|
|||
|
||||
val mustangVersion: String by project
|
||||
|
||||
val angusMailVersion: String by project
|
||||
|
||||
val klfVersion: String by project
|
||||
|
||||
val assertKVersion: String by project
|
||||
|
@ -19,6 +21,8 @@ val logbackVersion: String by project
|
|||
dependencies {
|
||||
implementation("org.mustangproject:library:$mustangVersion")
|
||||
|
||||
implementation("org.eclipse.angus:angus-mail:$angusMailVersion")
|
||||
|
||||
implementation("net.codinux.log:klf:$klfVersion")
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
class MailAccount(
|
||||
val username: String,
|
||||
val password: String,
|
||||
/**
|
||||
* For reading mails the IMAP server address, for sending mails the SMTP server address.
|
||||
*/
|
||||
val serverAddress: String,
|
||||
/**
|
||||
* Even though not mandatory it's better to specify the port, otherwise default port is tried.
|
||||
*/
|
||||
val port: Int? = null
|
||||
) {
|
||||
override fun toString() = "$username $serverAddress${port?.let { ":$it" } ?: ""}"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import net.codinux.invoicing.model.Invoice
|
||||
import java.io.File
|
||||
|
||||
class MailAttachmentWithEInvoice(
|
||||
val filename: String,
|
||||
val contentType: String,
|
||||
val invoice: Invoice,
|
||||
val file: File
|
||||
) {
|
||||
override fun toString() = "$filename: $invoice"
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import jakarta.mail.BodyPart
|
||||
import jakarta.mail.Folder
|
||||
import jakarta.mail.Part
|
||||
import jakarta.mail.Session
|
||||
import jakarta.mail.internet.MimeMultipart
|
||||
import net.codinux.invoicing.model.Invoice
|
||||
import net.codinux.invoicing.reader.EInvoiceReader
|
||||
import net.codinux.log.logger
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
|
||||
class MailReader(
|
||||
private val eInvoiceReader: EInvoiceReader = EInvoiceReader()
|
||||
) {
|
||||
|
||||
private val extractionErrorMessages = mutableSetOf<String?>()
|
||||
|
||||
private val extractionErrors = mutableSetOf<Throwable>()
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
fun listAllMessagesWithEInvoice(account: MailAccount): List<MailWithInvoice> {
|
||||
val properties = mapAccountToJavaMailProperties(account)
|
||||
|
||||
try {
|
||||
val session = Session.getInstance(properties)
|
||||
session.getStore("imap").use { store ->
|
||||
store.connect(account.serverAddress, account.username, account.password)
|
||||
|
||||
val inbox = store.getFolder("INBOX")
|
||||
inbox.open(Folder.READ_ONLY)
|
||||
|
||||
return listAllMessagesWithEInvoiceInFolder(inbox).also {
|
||||
inbox.close(false)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not read mails of account $account" }
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun mapAccountToJavaMailProperties(account: MailAccount) = Properties().apply {
|
||||
put("mail.store.protocol", "imap")
|
||||
|
||||
put("mail.imap.host", account.serverAddress)
|
||||
put("mail.imap.port", account.port?.toString() ?: "993") // Default IMAP over SSL
|
||||
put("mail.imap.ssl.enable", "true")
|
||||
|
||||
put("mail.imap.connectiontimeout", "5000")
|
||||
put("mail.imap.timeout", "5000")
|
||||
}
|
||||
|
||||
|
||||
private fun listAllMessagesWithEInvoiceInFolder(folder: Folder): List<MailWithInvoice> = folder.messages.mapNotNull { message ->
|
||||
try {
|
||||
if (message.isMimeType("multipart/*")) {
|
||||
val multipart = message.content as MimeMultipart
|
||||
val parts = IntRange(0, multipart.count - 1).map { multipart.getBodyPart(it) }
|
||||
|
||||
val attachmentsWithEInvoice = parts.mapNotNull { part ->
|
||||
findEInvoice(part)
|
||||
}
|
||||
|
||||
if (attachmentsWithEInvoice.isNotEmpty()) {
|
||||
return@mapNotNull MailWithInvoice(message.from.joinToString(), message.subject, map(message.sentDate), attachmentsWithEInvoice)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not read mail $message" }
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
private fun findEInvoice(part: BodyPart): MailAttachmentWithEInvoice? {
|
||||
try {
|
||||
if (part.disposition == Part.ATTACHMENT) {
|
||||
val invoice = tryToReadEInvoice(part)
|
||||
if (invoice != null) {
|
||||
var contentType = part.contentType
|
||||
val indexOfSeparator = contentType.indexOf(';')
|
||||
if (indexOfSeparator > -1) {
|
||||
contentType = contentType.substring(0, indexOfSeparator)
|
||||
}
|
||||
|
||||
val filename = File(part.fileName)
|
||||
val file = File.createTempFile(filename.nameWithoutExtension, filename.extension).also { file ->
|
||||
part.inputStream.use { it.copyTo(file.outputStream()) }
|
||||
}
|
||||
|
||||
return MailAttachmentWithEInvoice(part.fileName, contentType, invoice, file)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not check attachment '${part.fileName}' for eInvoice" }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun tryToReadEInvoice(part: BodyPart): Invoice? {
|
||||
val filename = part.fileName.lowercase()
|
||||
val contentType = part.contentType.lowercase()
|
||||
|
||||
return if (filename.endsWith(".pdf") || contentType.startsWith("application/pdf") || contentType.startsWith("application/octet-stream")) {
|
||||
try {
|
||||
eInvoiceReader.extractFromPdf(part.inputStream)
|
||||
} catch (e: Throwable) {
|
||||
extractionErrorMessages.add(e.message)
|
||||
extractionErrors.add(e)
|
||||
null
|
||||
}
|
||||
} else if (filename.endsWith(".xml") || contentType.startsWith("application/xml") || contentType.startsWith("text/xml")) {
|
||||
eInvoiceReader.readFromXml(part.inputStream)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: same code as in MustangMapper
|
||||
private fun map(date: Date): LocalDate =
|
||||
date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
class MailWithInvoice(
|
||||
val sender: String,
|
||||
val subject: String,
|
||||
val date: LocalDate,
|
||||
val attachmentsWithEInvoice: List<MailAttachmentWithEInvoice>
|
||||
) {
|
||||
override fun toString() = "$date $sender: $subject, ${attachmentsWithEInvoice.size} invoices"
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNotEmpty
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore // not an automatic test, set your mail account settings below
|
||||
class MailReaderTest {
|
||||
|
||||
companion object {
|
||||
// specify your mail account here
|
||||
private val mailAccount = MailAccount(
|
||||
username = "",
|
||||
password = "",
|
||||
serverAddress = "",
|
||||
port = null // can be left as null if default port 993 is used
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private val underTest = MailReader()
|
||||
|
||||
|
||||
@Test
|
||||
fun listAllMessagesWithEInvoice() {
|
||||
val result = underTest.listAllMessagesWithEInvoice(mailAccount)
|
||||
|
||||
assertThat(result).isNotEmpty()
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,8 @@ kotlinVersion=1.9.25
|
|||
|
||||
mustangVersion=2.14.2
|
||||
|
||||
angusMailVersion=2.0.3
|
||||
|
||||
klfVersion=1.6.2
|
||||
# only used for tests
|
||||
logbackVersion=1.5.12
|
||||
|
|
Loading…
Reference in New Issue