From 0c1d48736ca8a6d7250e64252ec4e9ce2147b48d Mon Sep 17 00:00:00 2001 From: dankito Date: Tue, 26 Nov 2024 01:34:26 +0100 Subject: [PATCH] Added errors to result of listAllMessagesWithEInvoice() --- .../net/codinux/invoicing/mail/MailReader.kt | 72 +++++++++++-------- .../codinux/invoicing/mail/ReadMailError.kt | 7 ++ .../invoicing/mail/ReadMailsErrorType.kt | 13 ++++ .../invoicing/mail/ReadMailsOptions.kt | 5 ++ .../codinux/invoicing/mail/ReadMailsResult.kt | 7 ++ .../codinux/invoicing/mail/ReadMailsStatus.kt | 39 ++++++++++ .../codinux/invoicing/mail/MailReaderTest.kt | 6 +- 7 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailError.kt create mode 100644 e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsErrorType.kt create mode 100644 e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsOptions.kt create mode 100644 e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsResult.kt create mode 100644 e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsStatus.kt diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt index d2c7667..a0a9820 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt @@ -34,16 +34,19 @@ open class MailReader( protected val log by logger() - open fun listenForNewReceivedEInvoices(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", eInvoiceReceived: (MailWithInvoice) -> Unit) = runBlocking { + open fun listenForNewReceivedEInvoices(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", + error: ((ReadMailError) -> Unit)? = null, eInvoiceReceived: (MailWithInvoice) -> Unit) = runBlocking { try { connect(account) { store -> val folder = store.getFolder(emailFolderName) folder.open(Folder.READ_ONLY) + val status = ReadMailsStatus(ReadMailsOptions(downloadMessageBody)) + folder.addMessageCountListener(object : MessageCountAdapter() { override fun messagesAdded(event: MessageCountEvent) { event.messages.forEach { message -> - findEInvoice(message, downloadMessageBody)?.let { + findEInvoice(message, status)?.let { eInvoiceReceived(it) } } @@ -56,6 +59,7 @@ open class MailReader( } } catch (e: Throwable) { log.error(e) { "Listening to new received eInvoices of '${account.username}' failed" } + error?.invoke(ReadMailError(ReadMailsErrorType.ListenForNewEmails, null, e)) } log.info { "Stopped listening to new received eInvoices of '${account.username}'" } @@ -78,24 +82,28 @@ open class MailReader( } - open fun listAllMessagesWithEInvoice(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): List { + open fun listAllMessagesWithEInvoice(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): ReadMailsResult { try { return connect(account) { store -> val inbox = store.getFolder(emailFolderName) inbox.open(Folder.READ_ONLY) - listAllMessagesWithEInvoiceInFolder(inbox, downloadMessageBody).also { + val status = ReadMailsStatus(ReadMailsOptions(downloadMessageBody)) + + val mails = listAllMessagesWithEInvoiceInFolder(inbox, status).also { inbox.close(false) } + + ReadMailsResult(mails, null, status.mailSpecificErrors) } } catch (e: Throwable) { log.error(e) { "Could not read mails of account $account" } - } - return emptyList() + return ReadMailsResult(emptyList(), e) + } } - protected open fun listAllMessagesWithEInvoiceInFolder(folder: Folder, downloadMessageBody: Boolean): List = runBlocking { + protected open fun listAllMessagesWithEInvoiceInFolder(folder: Folder, status: ReadMailsStatus): List = runBlocking { val messageCount = folder.messageCount if (messageCount <= 0) { return@runBlocking emptyList() @@ -104,9 +112,10 @@ open class MailReader( IntRange(1, messageCount).mapNotNull { messageNumber -> // message numbers start at 1 async(mailDispatcher) { try { - findEInvoice(folder.getMessage(messageNumber), downloadMessageBody) + findEInvoice(folder.getMessage(messageNumber), status) } catch (e: Throwable) { log.error(e) { "Could not get message with messageNumber $messageNumber" } + status.addError(ReadMailsErrorType.GetEmail, messageNumber, e) null } } @@ -115,34 +124,30 @@ open class MailReader( .filterNotNull() } - protected open fun findEInvoice(message: Message, downloadMessageBody: Boolean): MailWithInvoice? { - try { - val parts = getAllMessageParts(message) + protected open fun findEInvoice(message: Message, status: ReadMailsStatus): MailWithInvoice? { + val parts = getAllMessageParts(message) - val attachmentsWithEInvoice = parts.mapNotNull { part -> - findEInvoice(part) - } + val attachmentsWithEInvoice = parts.mapNotNull { part -> + findEInvoice(part, status) + } - if (attachmentsWithEInvoice.isNotEmpty()) { - return MailWithInvoice( - message.from?.joinToString(), message.subject ?: "", - message.sentDate?.let { map(it) }, map(message.receivedDate), message.messageNumber, - parts.any { it.mediaType == "application/pgp-encrypted" }, - if (downloadMessageBody) getPlainTextBody(parts) else null, if (downloadMessageBody) getHtmlBody(parts) else null, - attachmentsWithEInvoice - ) - } - } catch (e: Throwable) { - log.error(e) { "Could not read message $message" } + if (attachmentsWithEInvoice.isNotEmpty()) { + return MailWithInvoice( + message.from?.joinToString(), message.subject ?: "", + message.sentDate?.let { map(it) }, map(message.receivedDate), message.messageNumber, + parts.any { it.mediaType == "application/pgp-encrypted" }, + getPlainTextBody(parts, status), getHtmlBody(parts, status), + attachmentsWithEInvoice + ) } return null } - protected open fun findEInvoice(messagePart: MessagePart): MailAttachmentWithEInvoice? { + protected open fun findEInvoice(messagePart: MessagePart, status: ReadMailsStatus): MailAttachmentWithEInvoice? { try { val part = messagePart.part - val invoice = tryToReadEInvoice(part, messagePart.mediaType) + val invoice = tryToReadEInvoice(part, messagePart.mediaType, status) if (invoice != null) { val filename = File(part.fileName) @@ -155,12 +160,13 @@ open class MailReader( } } catch (e: Throwable) { log.error(e) { "Could not check attachment '${messagePart.part.fileName}' (${messagePart.mediaType}) for eInvoice" } + status.addError(ReadMailsErrorType.GetAttachment, messagePart.part, e) } return null } - protected open fun tryToReadEInvoice(part: Part, mediaType: String?): Invoice? = try { + protected open fun tryToReadEInvoice(part: Part, mediaType: String?, status: ReadMailsStatus): Invoice? = try { val filename = part.fileName?.lowercase() ?: "" if (filename.endsWith(".pdf") || mediaType == "application/pdf" || mediaType == "application/octet-stream") { @@ -172,6 +178,7 @@ open class MailReader( } } catch (e: Throwable) { log.debug(e) { "Could not extract invoices from ${part.fileName}" } + status.addError(ReadMailsErrorType.ExtractInvoice, part, e) null } @@ -212,11 +219,13 @@ open class MailReader( } } - protected open fun getPlainTextBody(parts: Collection) = getBodyWithMediaType(parts, "text/plain") + protected open fun getPlainTextBody(parts: Collection, status: ReadMailsStatus) = + if (status.options.downloadMessageBody) getBodyWithMediaType(parts, "text/plain", status) else null - protected open fun getHtmlBody(parts: Collection) = getBodyWithMediaType(parts, "text/html") + protected open fun getHtmlBody(parts: Collection, status: ReadMailsStatus) = + if (status.options.downloadMessageBody) getBodyWithMediaType(parts, "text/html", status) else null - protected open fun getBodyWithMediaType(parts: Collection, mediaType: String): String? = try { + protected open fun getBodyWithMediaType(parts: Collection, mediaType: String, status: ReadMailsStatus): String? = try { val partsForMediaType = parts.filter { it.mediaType == mediaType } if (partsForMediaType.size == 1) { @@ -236,6 +245,7 @@ open class MailReader( } } catch (e: Throwable) { log.error(e) { "Could not get message body for media type '$mediaType'" } + status.addError(ReadMailsErrorType.GetMesssageBody, parts.map { it.part }, e) null } diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailError.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailError.kt new file mode 100644 index 0000000..32d21f2 --- /dev/null +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailError.kt @@ -0,0 +1,7 @@ +package net.codinux.invoicing.mail + +data class ReadMailError( + val type: ReadMailsErrorType, + val messageNumber: Int?, + val error: Throwable +) \ No newline at end of file diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsErrorType.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsErrorType.kt new file mode 100644 index 0000000..6b6a9e8 --- /dev/null +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsErrorType.kt @@ -0,0 +1,13 @@ +package net.codinux.invoicing.mail + +enum class ReadMailsErrorType { + GetEmail, + + GetMesssageBody, + + GetAttachment, + + ExtractInvoice, + + ListenForNewEmails +} \ No newline at end of file diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsOptions.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsOptions.kt new file mode 100644 index 0000000..7119636 --- /dev/null +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsOptions.kt @@ -0,0 +1,5 @@ +package net.codinux.invoicing.mail + +data class ReadMailsOptions( + val downloadMessageBody: Boolean = false +) \ No newline at end of file diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsResult.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsResult.kt new file mode 100644 index 0000000..ffccc2a --- /dev/null +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsResult.kt @@ -0,0 +1,7 @@ +package net.codinux.invoicing.mail + +data class ReadMailsResult( + val emails: List, + val overallError: Throwable?, + val messageSpecificErrors: List = emptyList() +) \ No newline at end of file diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsStatus.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsStatus.kt new file mode 100644 index 0000000..c6c1eb0 --- /dev/null +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsStatus.kt @@ -0,0 +1,39 @@ +package net.codinux.invoicing.mail + +import jakarta.mail.BodyPart +import jakarta.mail.Message +import jakarta.mail.Part + +data class ReadMailsStatus( + val options: ReadMailsOptions, + val mailSpecificErrors: MutableList = mutableListOf(), + val error: ((ReadMailError) -> Unit)? = null +) { + + fun addError(type: ReadMailsErrorType, parts: Collection, error: Throwable) = + addError(ReadMailError(type, parts.firstNotNullOfOrNull { getMessage(it) }?.messageNumber, error)) + + fun addError(type: ReadMailsErrorType, part: Part, error: Throwable) = + addError(ReadMailError(type, getMessage(part)?.messageNumber, error)) + + fun addError(type: ReadMailsErrorType, messageNumber: Int?, error: Throwable) = + addError(ReadMailError(type, messageNumber, error)) + + fun addError(mailError: ReadMailError) { + mailSpecificErrors.add(mailError) + + error?.invoke(mailError) + } + + private fun getMessage(part: Part): Message? { + if (part is Message) { + return part + } + + (part as? BodyPart)?.parent.let { parent -> + return getMessage(part) + } + + return null + } +} \ No newline at end of file diff --git a/e-invoice-domain/src/test/kotlin/net/codinux/invoicing/mail/MailReaderTest.kt b/e-invoice-domain/src/test/kotlin/net/codinux/invoicing/mail/MailReaderTest.kt index 1362873..4bfb9a7 100644 --- a/e-invoice-domain/src/test/kotlin/net/codinux/invoicing/mail/MailReaderTest.kt +++ b/e-invoice-domain/src/test/kotlin/net/codinux/invoicing/mail/MailReaderTest.kt @@ -27,10 +27,10 @@ class MailReaderTest { fun listAllMessagesWithEInvoice() { val result = underTest.listAllMessagesWithEInvoice(mailAccount, true) - assertThat(result).isNotEmpty() + assertThat(result.emails).isNotEmpty() - val messagesWithoutBody = result.filter { it.plainTextOrHtmlBody == null } - assertThat(messagesWithoutBody).isEmpty() + val emailsWithoutBody = result.emails.filter { it.plainTextOrHtmlBody == null } + assertThat(emailsWithoutBody).isEmpty() } } \ No newline at end of file