diff --git a/README.md b/README.md index e71e7ff..85ee90c 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ val invoiceFromXml = reader.extractFromXml(File("XRechnung.xml")) ### Find all invoices of an IMAP email account ```kotlin -val mailReader = MailReader() +val emailsFetcher = EmailsFetcher() -val mailsWithEInvoices = mailReader.listAllMessagesWithEInvoice(MailAccount( +val mailsWithEInvoices = emailsFetcher.listAllMessagesWithEInvoice(EmailAccount( username = "", // your mail account username password = "", // your mail account username serverAddress = "", // IMAP server address diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailAccount.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailAccount.kt similarity index 64% rename from e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailAccount.kt rename to e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailAccount.kt index 2c4c004..b62b402 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailAccount.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailAccount.kt @@ -1,14 +1,14 @@ package net.codinux.invoicing.mail -class MailAccount( +class EmailAccount( val username: String, val password: String, /** - * For reading mails the IMAP server address, for sending mails the SMTP server address. + * For fetching emails the IMAP server address, for sending emails the SMTP server address. */ val serverAddress: String, /** - * Even though not mandatory it's better to specify the port, otherwise default port is tried. + * Even though not mandatory it's better to specify the port, otherwise default port (993 for IMAP, 587 for SMTP) is used. */ val port: Int? = null ) { diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailAttachmentWithEInvoice.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailAttachmentWithEInvoice.kt similarity index 92% rename from e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailAttachmentWithEInvoice.kt rename to e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailAttachmentWithEInvoice.kt index 177449a..5bbd759 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailAttachmentWithEInvoice.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailAttachmentWithEInvoice.kt @@ -3,7 +3,7 @@ package net.codinux.invoicing.mail import net.codinux.invoicing.model.Invoice import java.io.File -class MailAttachmentWithEInvoice( +class EmailAttachmentWithEInvoice( val filename: String, /** * Attachment's media type like "application/xml", "application/pdf", ... diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailWithInvoice.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailWithInvoice.kt similarity index 87% rename from e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailWithInvoice.kt rename to e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailWithInvoice.kt index 6c85a64..56d8192 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailWithInvoice.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailWithInvoice.kt @@ -3,7 +3,7 @@ package net.codinux.invoicing.mail import java.time.Instant import java.time.ZoneId -class MailWithInvoice( +class EmailWithInvoice( val sender: String?, val subject: String, val sent: Instant?, @@ -13,13 +13,13 @@ class MailWithInvoice( * "Since message numbers can change within a session if the folder is expunged, clients are advised not to use * message numbers as references to messages." * - * -> use with care. Message numbers are not valid / the same anymore after expunge. + * -> use with care. Message numbers are not valid / the same anymore after expunging. */ val messageNumber: Int, val isEncrypted: Boolean = false, val plainTextBody: String?, val htmlBody: String?, - val attachmentsWithEInvoice: List + val attachmentsWithEInvoice: List ) { val plainTextOrHtmlBody: String? by lazy { plainTextBody ?: htmlBody } 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/EmailsFetcher.kt similarity index 81% rename from e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt rename to e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/EmailsFetcher.kt index ed6adfc..2b916bc 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/EmailsFetcher.kt @@ -19,7 +19,7 @@ import java.util.* import java.util.concurrent.Executors import kotlin.math.max -open class MailReader( +open class EmailsFetcher( protected open val eInvoiceReader: EInvoiceReader = EInvoiceReader() ) { @@ -34,14 +34,14 @@ open class MailReader( protected val log by logger() - open fun listenForNewReceivedEInvoices(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", - error: ((ReadMailError) -> Unit)? = null, eInvoiceReceived: (MailWithInvoice) -> Unit) = runBlocking { + open fun listenForNewReceivedEInvoices(account: EmailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", + error: ((FetchEmailsError) -> Unit)? = null, eInvoiceReceived: (EmailWithInvoice) -> Unit) = runBlocking { try { connect(account) { store -> val folder = store.getFolder(emailFolderName) folder.open(Folder.READ_ONLY) - val status = ReadMailsStatus(ReadMailsOptions(downloadMessageBody)) + val status = FetchEmailsStatus(FetchEmailsOptions(downloadMessageBody)) folder.addMessageCountListener(object : MessageCountAdapter() { override fun messagesAdded(event: MessageCountEvent) { @@ -59,13 +59,13 @@ 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)) + error?.invoke(FetchEmailsError(FetchEmailsErrorType.ListenForNewEmails, null, e)) } log.info { "Stopped listening to new received eInvoices of '${account.username}'" } } - protected open suspend fun keepConnectionOpen(account: MailAccount, folder: Folder) { + protected open suspend fun keepConnectionOpen(account: EmailAccount, folder: Folder) { log.info { "Listening to new mails of ${account.username}" } // Use IMAP IDLE to keep the connection alive @@ -82,28 +82,28 @@ open class MailReader( } - open fun listAllMessagesWithEInvoice(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): ReadMailsResult { + open fun listAllMessagesWithEInvoice(account: EmailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): FetchEmailsResult { try { return connect(account) { store -> val inbox = store.getFolder(emailFolderName) inbox.open(Folder.READ_ONLY) - val status = ReadMailsStatus(ReadMailsOptions(downloadMessageBody)) + val status = FetchEmailsStatus(FetchEmailsOptions(downloadMessageBody)) val mails = listAllMessagesWithEInvoiceInFolder(inbox, status).also { inbox.close(false) } - ReadMailsResult(mails, null, status.mailSpecificErrors) + FetchEmailsResult(mails, null, status.messageSpecificErrors) } } catch (e: Throwable) { log.error(e) { "Could not read mails of account $account" } - return ReadMailsResult(emptyList(), e) + return FetchEmailsResult(emptyList(), e) } } - protected open fun listAllMessagesWithEInvoiceInFolder(folder: Folder, status: ReadMailsStatus): List = runBlocking { + protected open fun listAllMessagesWithEInvoiceInFolder(folder: Folder, status: FetchEmailsStatus): List = runBlocking { val messageCount = folder.messageCount if (messageCount <= 0) { return@runBlocking emptyList() @@ -115,7 +115,7 @@ open class MailReader( findEInvoice(folder.getMessage(messageNumber), status) } catch (e: Throwable) { log.error(e) { "Could not get message with messageNumber $messageNumber" } - status.addError(ReadMailsErrorType.GetEmail, messageNumber, e) + status.addError(FetchEmailsErrorType.GetEmail, messageNumber, e) null } } @@ -124,7 +124,7 @@ open class MailReader( .filterNotNull() } - protected open fun findEInvoice(message: Message, status: ReadMailsStatus): MailWithInvoice? { + protected open fun findEInvoice(message: Message, status: FetchEmailsStatus): EmailWithInvoice? { val parts = getAllMessageParts(message) val attachmentsWithEInvoice = parts.mapNotNull { part -> @@ -132,7 +132,7 @@ open class MailReader( } if (attachmentsWithEInvoice.isNotEmpty()) { - return MailWithInvoice( + return EmailWithInvoice( message.from?.joinToString(), message.subject ?: "", message.sentDate?.let { map(it) }, map(message.receivedDate), message.messageNumber, parts.any { it.mediaType == "application/pgp-encrypted" }, @@ -144,7 +144,7 @@ open class MailReader( return null } - protected open fun findEInvoice(messagePart: MessagePart, status: ReadMailsStatus): MailAttachmentWithEInvoice? { + protected open fun findEInvoice(messagePart: MessagePart, status: FetchEmailsStatus): EmailAttachmentWithEInvoice? { try { val part = messagePart.part val invoice = tryToReadEInvoice(part, messagePart.mediaType, status) @@ -156,17 +156,17 @@ open class MailReader( file.deleteOnExit() } - return MailAttachmentWithEInvoice(part.fileName, messagePart.mediaType, invoice, file) + return EmailAttachmentWithEInvoice(part.fileName, messagePart.mediaType, invoice, file) } } catch (e: Throwable) { log.error(e) { "Could not check attachment '${messagePart.part.fileName}' (${messagePart.mediaType}) for eInvoice" } - status.addError(ReadMailsErrorType.GetAttachment, messagePart.part, e) + status.addError(FetchEmailsErrorType.GetAttachment, messagePart.part, e) } return null } - protected open fun tryToReadEInvoice(part: Part, mediaType: String?, status: ReadMailsStatus): Invoice? = try { + protected open fun tryToReadEInvoice(part: Part, mediaType: String?, status: FetchEmailsStatus): Invoice? = try { val filename = part.fileName?.lowercase() ?: "" if (filename.endsWith(".pdf") || mediaType == "application/pdf" || mediaType == "application/octet-stream") { @@ -178,7 +178,7 @@ open class MailReader( } } catch (e: Throwable) { log.debug(e) { "Could not extract invoices from ${part.fileName}" } - status.addError(ReadMailsErrorType.ExtractInvoice, part, e) + status.addError(FetchEmailsErrorType.ExtractInvoice, part, e) null } @@ -219,13 +219,13 @@ open class MailReader( } } - protected open fun getPlainTextBody(parts: Collection, status: ReadMailsStatus) = + protected open fun getPlainTextBody(parts: Collection, status: FetchEmailsStatus) = if (status.options.downloadMessageBody) getBodyWithMediaType(parts, "text/plain", status) else null - protected open fun getHtmlBody(parts: Collection, status: ReadMailsStatus) = + protected open fun getHtmlBody(parts: Collection, status: FetchEmailsStatus) = if (status.options.downloadMessageBody) getBodyWithMediaType(parts, "text/html", status) else null - protected open fun getBodyWithMediaType(parts: Collection, mediaType: String, status: ReadMailsStatus): String? = try { + protected open fun getBodyWithMediaType(parts: Collection, mediaType: String, status: FetchEmailsStatus): String? = try { val partsForMediaType = parts.filter { it.mediaType == mediaType } if (partsForMediaType.size == 1) { @@ -245,7 +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) + status.addError(FetchEmailsErrorType.GetMesssageBody, parts.map { it.part }, e) null } @@ -253,7 +253,7 @@ open class MailReader( date.toInstant() - protected open fun connect(account: MailAccount, connected: (Store) -> T): T { + protected open fun connect(account: EmailAccount, connected: (Store) -> T): T { val properties = mapAccountToJavaMailProperties(account) val session = Session.getInstance(properties) @@ -264,7 +264,7 @@ open class MailReader( } } - protected open fun mapAccountToJavaMailProperties(account: MailAccount) = Properties().apply { + protected open fun mapAccountToJavaMailProperties(account: EmailAccount) = Properties().apply { put("mail.store.protocol", "imap") put("mail.imap.host", account.serverAddress) 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/FetchEmailsError.kt similarity index 57% rename from e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailError.kt rename to e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsError.kt index 32d21f2..e39c29b 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailError.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsError.kt @@ -1,7 +1,7 @@ package net.codinux.invoicing.mail -data class ReadMailError( - val type: ReadMailsErrorType, +data class FetchEmailsError( + val type: FetchEmailsErrorType, 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/FetchEmailsErrorType.kt similarity index 79% rename from e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsErrorType.kt rename to e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsErrorType.kt index 6b6a9e8..da1ce6b 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsErrorType.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsErrorType.kt @@ -1,6 +1,6 @@ package net.codinux.invoicing.mail -enum class ReadMailsErrorType { +enum class FetchEmailsErrorType { GetEmail, GetMesssageBody, 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/FetchEmailsOptions.kt similarity index 71% rename from e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsOptions.kt rename to e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsOptions.kt index 7119636..3e05f00 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsOptions.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsOptions.kt @@ -1,5 +1,5 @@ package net.codinux.invoicing.mail -data class ReadMailsOptions( +data class FetchEmailsOptions( val downloadMessageBody: Boolean = false ) \ No newline at end of file diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsResult.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsResult.kt new file mode 100644 index 0000000..2c504d6 --- /dev/null +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsResult.kt @@ -0,0 +1,7 @@ +package net.codinux.invoicing.mail + +data class FetchEmailsResult( + 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/FetchEmailsStatus.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsStatus.kt new file mode 100644 index 0000000..f8e46d6 --- /dev/null +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/FetchEmailsStatus.kt @@ -0,0 +1,39 @@ +package net.codinux.invoicing.mail + +import jakarta.mail.BodyPart +import jakarta.mail.Message +import jakarta.mail.Part + +data class FetchEmailsStatus( + val options: FetchEmailsOptions, + val messageSpecificErrors: MutableList = mutableListOf(), + val error: ((FetchEmailsError) -> Unit)? = null +) { + + fun addError(type: FetchEmailsErrorType, parts: Collection, error: Throwable) = + addError(FetchEmailsError(type, parts.firstNotNullOfOrNull { getMessage(it) }?.messageNumber, error)) + + fun addError(type: FetchEmailsErrorType, part: Part, error: Throwable) = + addError(FetchEmailsError(type, getMessage(part)?.messageNumber, error)) + + fun addError(type: FetchEmailsErrorType, messageNumber: Int?, error: Throwable) = + addError(FetchEmailsError(type, messageNumber, error)) + + fun addError(mailError: FetchEmailsError) { + messageSpecificErrors.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/main/kotlin/net/codinux/invoicing/mail/ReadMailsResult.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsResult.kt deleted file mode 100644 index ffccc2a..0000000 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index c6c1eb0..0000000 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/ReadMailsStatus.kt +++ /dev/null @@ -1,39 +0,0 @@ -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/Demonstration.kt b/e-invoice-domain/src/test/kotlin/net/codinux/invoicing/Demonstration.kt index 091cc0c..5b7edb3 100644 --- a/e-invoice-domain/src/test/kotlin/net/codinux/invoicing/Demonstration.kt +++ b/e-invoice-domain/src/test/kotlin/net/codinux/invoicing/Demonstration.kt @@ -1,8 +1,8 @@ package net.codinux.invoicing import net.codinux.invoicing.creation.EInvoiceCreator -import net.codinux.invoicing.mail.MailAccount -import net.codinux.invoicing.mail.MailReader +import net.codinux.invoicing.mail.EmailAccount +import net.codinux.invoicing.mail.EmailsFetcher import net.codinux.invoicing.model.Invoice import net.codinux.invoicing.model.InvoiceItem import net.codinux.invoicing.model.Party @@ -24,10 +24,10 @@ class Demonstration { val invoiceFromXml = reader.extractFromXml(File("XRechnung.xml")) } - fun fromMail() { - val mailReader = MailReader() + fun fromEmail() { + val emailsFetcher = EmailsFetcher() - val mailsWithEInvoices = mailReader.listAllMessagesWithEInvoice(MailAccount( + val mailsWithEInvoices = emailsFetcher.listAllMessagesWithEInvoice(EmailAccount( username = "", // your mail account username password = "", // your mail account username serverAddress = "", // IMAP server address 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/EmailsFetcherTest.kt similarity index 79% rename from e-invoice-domain/src/test/kotlin/net/codinux/invoicing/mail/MailReaderTest.kt rename to e-invoice-domain/src/test/kotlin/net/codinux/invoicing/mail/EmailsFetcherTest.kt index bac0648..e909627 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/EmailsFetcherTest.kt @@ -7,11 +7,11 @@ import org.junit.jupiter.api.Test import kotlin.test.Ignore @Ignore // not an automatic test, set your mail account settings below -class MailReaderTest { +class EmailsFetcherTest { companion object { // specify your mail account here - private val mailAccount = MailAccount( + private val emailAccount = EmailAccount( username = "", password = "", serverAddress = "", @@ -20,12 +20,12 @@ class MailReaderTest { } - private val underTest = MailReader() + private val underTest = EmailsFetcher() @Test fun listAllMessagesWithEInvoice() { - val result = underTest.listAllMessagesWithEInvoice(mailAccount, true) + val result = underTest.listAllMessagesWithEInvoice(emailAccount, true) assertThat(result.emails).isNotEmpty()