├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── LICENSE ├── README.MD ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jreleaser.yml ├── logo.png ├── publiccode.yml ├── settings.gradle └── src ├── main └── java │ └── app │ └── tozzi │ ├── MailParser.java │ ├── core │ ├── DeliveryStatusHandler.java │ └── PECHandler.java │ ├── model │ ├── Address.java │ ├── Attachment.java │ ├── CertificateData.java │ ├── DataSourcePair.java │ ├── DeliveryStatus.java │ ├── Header.java │ ├── Mail.java │ ├── PEC.java │ ├── PECReceipt.java │ ├── ParsedEntity.java │ ├── ParsedEntityType.java │ └── exception │ │ └── MailParserException.java │ └── util │ ├── IOUtils.java │ ├── MailConstants.java │ ├── MimeMessageUtils.java │ ├── MimeTypesUtil.java │ ├── PECConstants.java │ ├── UUEncodingUtils.java │ └── XMLUtils.java └── test ├── java └── app │ └── tozzi │ ├── MailParserTest.java │ ├── core │ └── DeliveryStatusHandlerTest.java │ └── util │ ├── IOUtilsTest.java │ ├── MimeMessageUtilsTest.java │ ├── MimeTypesUtilTest.java │ ├── UUEncodingUtilsTest.java │ └── XMLUtilsTest.java └── resources ├── Delivery Status Notification (Failure).eml ├── Test - Simple Mail (2).eml ├── Test - Simple Mail (3).eml ├── Test - Simple Mail.eml ├── accettazione.eml └── consegna.eml /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | # Binary files should be left untouched 11 | *.jar binary 12 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - '**' 10 | release: 11 | types: [ created ] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up JDK 17 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: '17' 24 | distribution: 'temurin' 25 | 26 | - name: Cache Gradle packages 27 | uses: actions/cache@v4 28 | with: 29 | path: ~/.gradle/caches 30 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 31 | restore-keys: ${{ runner.os }}-gradle 32 | 33 | - name: Build with Gradle 34 | run: ./gradlew build -x test 35 | 36 | - name: Run tests 37 | run: ./gradlew test 38 | 39 | publish: 40 | needs: build 41 | if: github.event_name == 'release' 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | 49 | - name: Set up JDK 17 50 | uses: actions/setup-java@v4 51 | with: 52 | java-version: '17' 53 | distribution: 'temurin' 54 | server-id: central 55 | server-username: MAVEN_USERNAME 56 | server-password: MAVEN_CENTRAL_TOKEN 57 | 58 | - name: Publish 59 | run: | 60 | ./gradlew publish 61 | env: 62 | MAVEN_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} 63 | MAVEN_CENTRAL_TOKEN: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }} 64 | MAVEN_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 65 | 66 | - name: Release 67 | with: 68 | arguments: full-release 69 | uses: jreleaser/release-action@v2 70 | env: 71 | JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }} 72 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} 73 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 74 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} 75 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 76 | JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 77 | JRELEASER_PROJECT_VERSION: ${{ github.event.release.tag_name }} 78 | 79 | - name: Output 80 | uses: actions/upload-artifact@v4 81 | if: always() 82 | with: 83 | name: jreleaser-release 84 | path: | 85 | out/jreleaser/trace.log 86 | out/jreleaser/output.properties 87 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '0 0 * * 1' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze Codebase 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 360 16 | permissions: 17 | security-events: write 18 | packages: read 19 | actions: read 20 | contents: read 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | include: 26 | - language: java-kotlin 27 | build-mode: none 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v3 34 | with: 35 | languages: ${{ matrix.language }} 36 | build-mode: ${{ matrix.build-mode }} 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{matrix.language}}" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | /.idea/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # PEC/Mail Parser 2 | Libreria utility per l'elaborazione di messaggi di [Posta Elettronica Certificata](https://www.agid.gov.it/it/piattaforme/posta-elettronica-certificata) (PEC) e messaggi di posta ordinaria. 3 | 4 | ### Dipendenza progetto 5 | Attenzione: le versioni precedenti alla 5.0.0 sono state rilasciate con groupId `app.tozzi.mail` 6 | 7 | ##### Maven 8 | ``` 9 | 10 | app.tozzi 11 | pec-parser 12 | 5.1.2 13 | 14 | ``` 15 | 16 | ##### Gradle 17 | ``` 18 | implementation 'app.tozzi:pec-parser:5.1.2 19 | ``` 20 | 21 | ### Requisiti 22 | 23 | | PEC Parser | Java | 24 | |-------------------|-----------| 25 | | [v0.0.1 - v4.0.0] | [8 - 22] | 26 | | [v5.0.0 - latest] | [17 - 22] | 27 | 28 | ### Utilizzo 29 | #### 1. Istanza di `MailParser` 30 | Attraverso uno dei seguenti metodi: 31 | - [ ] `MailParser.getInstance()` 32 | - [ ] `MailParser.getInstance(Properties properties)` 33 | - [ ] `MailParser.getInstance(Properties properties, boolean extractAllHeaders)` 34 | - [ ] `MailParser.getInstance(boolean extractAllHeaders)` 35 | 36 | #### 2. Parsing 37 | Attraverso uno dei seguenti metodi: 38 | - [ ] `ParsedEntity parse(MimeMessage mimeMessage)` 39 | - [ ] `ParsedEntity parse(File eml)` 40 | - [ ] `ParsedEntity parse(InputStream inputStream)` 41 | 42 | #### 3. `ParsedEntity` 43 | Può essere di tipo: 44 | - [ ] `PEC`: se il messaggio rappresenta una PEC 45 | - [ ] `PECReceipt`: se il messaggio rappresenta una ricevuta PEC 46 | - [ ] `Mail`: se il messaggio rappresenta una semplice mail di posta ordinaria 47 | 48 | ### Altro 49 | - [ ] La libreria supporta l'elaborazione di messaggi di posta ordinaria con codifica [UUencode](https://en.wikipedia.org/wiki/Uuencoding) 50 | - [ ] La libreria è disponibile nel [catalogo Open Source di terze parti di Developers Italia](https://developers.italia.it/it/software/biagiot-java-pec-parser-09abab). 51 | 52 | ### Specifiche 53 | - [ ] Regole tecniche del servizio di trasmissione di documenti informatici mediante posta elettronica certificata: [pec_regole_tecniche_dm_2-nov-2005.pdf](https://www.agid.gov.it/sites/default/files/repository_files/leggi_decreti_direttive/pec_regole_tecniche_dm_2-nov-2005.pdf) 54 | 55 | ### Licenza 56 | 57 | - [ ] La licenza è disponibile [qui](https://github.com/biagioT/java-pec-parser/blob/main/LICENSE). 58 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'maven-publish' 4 | id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' 5 | } 6 | 7 | group = 'app.tozzi' 8 | version = '5.1.2' 9 | 10 | compileJava { 11 | sourceCompatibility = JavaVersion.VERSION_17 12 | targetCompatibility = JavaVersion.VERSION_17 13 | } 14 | 15 | java { 16 | sourceCompatibility = JavaVersion.VERSION_17 17 | targetCompatibility = JavaVersion.VERSION_17 18 | withSourcesJar() 19 | withJavadocJar() 20 | toolchain { 21 | languageVersion = JavaLanguageVersion.of(17) 22 | } 23 | } 24 | 25 | repositories { 26 | mavenCentral() 27 | } 28 | 29 | dependencies { 30 | // Java Mail 31 | implementation 'jakarta.mail:jakarta.mail-api:2.1.3' 32 | implementation 'org.eclipse.angus:angus-mail:2.0.3' 33 | 34 | // Java Activation 35 | implementation 'jakarta.activation:jakarta.activation-api:2.1.3' 36 | runtimeOnly 'org.eclipse.angus:angus-activation:2.0.2' 37 | 38 | // Lombok 39 | compileOnly 'org.projectlombok:lombok:1.18.38' 40 | annotationProcessor 'org.projectlombok:lombok:1.18.38' 41 | 42 | // Log 43 | implementation 'org.slf4j:slf4j-api:2.0.17' 44 | 45 | // Utils 46 | implementation 'org.apache.commons:commons-lang3:3.17.0' 47 | 48 | // XML 49 | implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' 50 | 51 | // Test 52 | testImplementation 'org.junit.jupiter:junit-jupiter:5.13.0' 53 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 54 | testImplementation 'org.mockito:mockito-core:5.18.0' 55 | testImplementation 'org.projectlombok:lombok:1.18.38' 56 | testCompileOnly 'org.projectlombok:lombok:1.18.38' 57 | testAnnotationProcessor 'org.projectlombok:lombok:1.18.38' 58 | } 59 | 60 | tasks.withType(JavaCompile) { 61 | options.encoding = 'UTF-8' 62 | } 63 | 64 | publishing { 65 | publications { 66 | mavenJava(MavenPublication) { 67 | group = 'app.tozzi' 68 | artifactId = 'pec-parser' 69 | version = "5.1.2" 70 | from components.java 71 | pom { 72 | name = 'pec-parser' 73 | description = 'Utility per l\'elaborazione di messaggi di Posta Elettronica Certificata (e messaggi di posta ordinaria)' 74 | url = 'https://www.tozzi.app' 75 | 76 | licenses { 77 | license { 78 | name = 'Apache License 2.0' 79 | url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' 80 | } 81 | } 82 | 83 | developers { 84 | developer { 85 | id = 'biagio.tozzi' 86 | name = 'Biagio Placido Tozzi' 87 | email = 'biagio.tozzi@gmail.com' 88 | url = 'https://www.tozzi.app' 89 | } 90 | } 91 | 92 | scm { 93 | connection = 'scm:git:git://github.com:biagioT/java-pec-parser.git' 94 | developerConnection = 'scm:git:ssh://github.com:biagioT/java-pec-parser.git' 95 | url = 'https://github.com/biagioT/java-pec-parser/tree/main' 96 | } 97 | } 98 | } 99 | } 100 | 101 | repositories { 102 | maven { 103 | url = layout.buildDirectory.dir('staging-deploy') 104 | } 105 | } 106 | } 107 | 108 | javadoc { 109 | options.addBooleanOption('html5', true) 110 | options.encoding = 'UTF-8' 111 | } 112 | 113 | test { 114 | useJUnitPlatform() 115 | } 116 | 117 | jar { 118 | enabled = true 119 | archiveClassifier.set('') 120 | } 121 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biagioT/java-pec-parser/74671119ffe280e71cb477e3110de8fcd4444abe/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /jreleaser.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: pec-parser 3 | description: Utility per l'elaborazione di messaggi di Posta Elettronica Certificata (e messaggi di posta ordinaria) 4 | longDescription: | 5 | Utility per l'elaborazione di messaggi di Posta Elettronica Certificata (e messaggi di posta ordinaria) 6 | inceptionYear: '2022' 7 | links: 8 | homepage: https://tozzi.app 9 | documentation: https://tozzi.app 10 | license: https://github.com/biagioT/java-pec-parser/blob/main/LICENSE 11 | authors: 12 | - Biagio Placido Tozzi 13 | license: APACHE-2.0 14 | tags: 15 | - java 16 | - mail 17 | - email 18 | - mail-parser 19 | - pec 20 | - rfc-6109 21 | - rfc-2821 22 | - legal-mail 23 | java: 24 | groupId: app.tozzi 25 | version: '17' 26 | 27 | signing: 28 | active: ALWAYS 29 | armored: true 30 | mode: MEMORY 31 | checksums: false 32 | 33 | release: 34 | github: 35 | skipRelease: true 36 | skipTag: true 37 | 38 | deploy: 39 | maven: 40 | mavenCentral: 41 | sonatype: 42 | active: RELEASE 43 | url: https://central.sonatype.com/api/v1/publisher 44 | stagingRepositories: 45 | - build/staging-deploy 46 | sign: true 47 | checksums: true 48 | sourceJar: true 49 | javadocJar: true 50 | applyMavenCentralRules: true 51 | 52 | checksum: 53 | individual: true 54 | 55 | 56 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biagioT/java-pec-parser/74671119ffe280e71cb477e3110de8fcd4444abe/logo.png -------------------------------------------------------------------------------- /publiccode.yml: -------------------------------------------------------------------------------- 1 | # This repository adheres to the publiccode.yml standard by including this 2 | # metadata file that makes public software easily discoverable. 3 | # More info at https://github.com/italia/publiccode.yml 4 | 5 | publiccodeYmlVersion: '0.2' 6 | categories: 7 | - communications 8 | - email-management 9 | description: 10 | it: 11 | apiDocumentation: 'https://github.com/biagioT/java-pec-parser' 12 | documentation: 'https://github.com/biagioT/java-pec-parser' 13 | features: 14 | - |- 15 | Elaborazione di messaggi di posta elettronica certificata PEC, ricevute 16 | PEC e posta ordinaria 17 | genericName: PEC/Mail Parser 18 | longDescription: | 19 | **PEC/Mail Parser** 20 | 21 | Libreria utility per l'elaborazione di messaggi di [Posta Elettronica 22 | Certificata](https://www.agid.gov.it/it/piattaforme/posta-elettronica-certificata) 23 | (PEC) e messaggi di posta ordinaria. 24 | 25 | **Dipendenza progetto** 26 | Attenzione: le versioni precedenti alla 5.0.0 sono state rilasciate con 27 | groupId \`app.tozzi.mail\` 28 | 29 | _Maven_ 30 | 31 | ` 32 | app.tozzi 33 | pec-parser 34 | 5.1.2 35 | ` 36 | 37 | 38 | _Gradle_ 39 | 40 | `implementation("app.tozzi.mail:pec-parser:5.1.2")` 41 | 42 | **Requisiti** 43 | 44 | 45 | - v0.0.1 - v4.0.0: Java 8 46 | 47 | - v5.0.0 - latest: Java 17 48 | 49 | 50 | **Utilizzo** 51 | 52 | Istanza di MailParser 53 | 54 | Attraverso uno dei seguenti metodi: 55 | 56 | 57 | - `MailParser.getInstance()` 58 | 59 | - `MailParser.getInstance(Properties properties)` 60 | 61 | - `MailParser.getInstance(Properties properties, boolean extractAllHeaders)` 62 | 63 | - `MailParser.getInstance(boolean extractAllHeaders)` 64 | 65 | 66 | Parsing: 67 | 68 | Attraverso uno dei seguenti metodi: 69 | 70 | - `ParsedEntity parse(MimeMessage mimeMessage)` 71 | 72 | - `ParsedEntity parse(File eml)` 73 | 74 | - `ParsedEntity parse(InputStream inputStream)` 75 | 76 | 77 | ParsedEntity: 78 | 79 | Può essere di tipo: 80 | 81 | - `PEC`: se il messaggio rappresenta una PEC 82 | 83 | - `PECReceipt`: se il messaggio rappresenta una ricevuta PEC 84 | 85 | - `Mail`: se il messaggio rappresenta una semplice mail di 86 | posta ordinaria 87 | 88 | 89 | **Altro** 90 | 91 | - La libreria supporta l'elaborazione di messaggi di posta ordinaria con 92 | codifica [UUencode](https://en.wikipedia.org/wiki/Uuencoding) 93 | 94 | 95 | **Specifiche** 96 | 97 | - Regole tecniche del servizio di trasmissione di documenti informatici 98 | mediante posta elettronica certificata: 99 | [pec\_regole\_tecniche\_dm\_2-nov-2005.pdf](https://www.agid.gov.it/sites/default/files/repository_files/leggi_decreti_direttive/pec_regole_tecniche_dm_2-nov-2005.pdf) 100 | shortDescription: |- 101 | Elaborazione di messaggi di posta elettronica certificata PEC, ricevute 102 | PEC e posta ordinaria. 103 | developmentStatus: stable 104 | intendedAudience: 105 | countries: 106 | - it 107 | - ch 108 | it: 109 | conforme: 110 | gdpr: false 111 | lineeGuidaDesign: false 112 | misureMinimeSicurezza: false 113 | modelloInteroperabilita: false 114 | countryExtensionVersion: '0.2' 115 | piattaforme: 116 | anpr: false 117 | cie: false 118 | pagopa: false 119 | spid: false 120 | landingURL: 'https://github.com/biagioT/java-pec-parser' 121 | legal: 122 | license: Apache-2.0 123 | mainCopyrightOwner: Biagio Placido Tozzi 124 | repoOwner: Biagio Placido Tozzi 125 | localisation: 126 | availableLanguages: 127 | - it 128 | - en 129 | localisationReady: true 130 | logo: |- 131 | https://raw.githubusercontent.com/biagioT/java-pec-parser/main/logo.png 132 | maintenance: 133 | contacts: 134 | - email: biagio.tozzi@gmail.com 135 | name: Biagio Placido Tozzi 136 | type: internal 137 | name: PEC Parser 138 | platforms: 139 | - web 140 | - windows 141 | - mac 142 | - linux 143 | - ios 144 | - android 145 | releaseDate: '2025-05-28' 146 | softwareType: standalone/web 147 | softwareVersion: 5.1.2 148 | url: 'https://github.com/biagioT/java-pec-parser' 149 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'java-pec-parser' -------------------------------------------------------------------------------- /src/main/java/app/tozzi/MailParser.java: -------------------------------------------------------------------------------- 1 | package app.tozzi; 2 | 3 | import app.tozzi.core.DeliveryStatusHandler; 4 | import app.tozzi.core.PECHandler; 5 | import app.tozzi.model.*; 6 | import app.tozzi.model.exception.MailParserException; 7 | import app.tozzi.util.*; 8 | import jakarta.activation.DataSource; 9 | import jakarta.mail.Multipart; 10 | import jakarta.mail.Part; 11 | import jakarta.mail.internet.InternetAddress; 12 | import jakarta.mail.internet.MimeMessage; 13 | import jakarta.mail.internet.MimePart; 14 | import lombok.AccessLevel; 15 | import lombok.AllArgsConstructor; 16 | 17 | import java.io.File; 18 | import java.io.FileInputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.util.Properties; 22 | import java.util.Spliterators; 23 | import java.util.stream.Stream; 24 | import java.util.stream.StreamSupport; 25 | 26 | /** 27 | * Main core class that offers email/PEC extraction 28 | * 29 | * @author Biagio Tozzi 30 | */ 31 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 32 | public class MailParser { 33 | 34 | private Properties properties; 35 | private boolean extractAllHeaders; 36 | 37 | /** 38 | * Default {@link MailParser} instance. 39 | * 43 | * 44 | * @return {@link MailParser} instance 45 | */ 46 | public static MailParser getInstance() { 47 | return new MailParser(null, false); 48 | } 49 | 50 | /** 51 | * {@link MailParser} instance with custom properties 52 | * 55 | * 56 | * @return {@link MailParser} instance 57 | */ 58 | public static MailParser getInstance(Properties properties) { 59 | return new MailParser(properties, false); 60 | } 61 | 62 | /** 63 | * {@link MailParser} instance with the extraction of all the headers 64 | * 65 | * @return {@link MailParser} instance 66 | */ 67 | public static MailParser getInstance(boolean extractAllHeaders) { 68 | return new MailParser(null, extractAllHeaders); 69 | } 70 | 71 | /** 72 | * {@link MailParser} instance with the extraction of all the headers and custom properties 73 | * 74 | * @return {@link MailParser} instance 75 | */ 76 | public static MailParser getInstance(Properties properties, boolean extractAllHeaders) { 77 | return new MailParser(properties, extractAllHeaders); 78 | } 79 | 80 | /** 81 | * Extracts a {@link ParsedEntity} from a mail MIME message.
82 | * ParsedEntity can be: 83 | * 88 | * 89 | * @param mimeMessage {@link MimeMessage} mail MIME message 90 | * @return {@link ParsedEntity} 91 | */ 92 | public ParsedEntity parse(MimeMessage mimeMessage) { 93 | 94 | var xTranspHeader = MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_TRASPORTO); 95 | var xReceiptHeader = MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_RICEVUTA); 96 | var parsedEntity = extract(mimeMessage, xTranspHeader != null, xReceiptHeader != null, this.properties, this.extractAllHeaders); 97 | 98 | if (xReceiptHeader != null) { 99 | return PECHandler.loadReceipt((PEC) parsedEntity); 100 | } 101 | 102 | return parsedEntity; 103 | } 104 | 105 | /** 106 | * Extracts a {@link ParsedEntity} from a mail message.
107 | * ParsedEntity can be: 108 | * 113 | * 114 | * @param eml {@link File} mail eml MIME message 115 | * @return {@link ParsedEntity} 116 | */ 117 | public ParsedEntity parse(File eml) { 118 | 119 | if (eml == null || !eml.exists()) { 120 | throw new MailParserException("Invalid file"); 121 | } 122 | 123 | try (InputStream is = new FileInputStream(eml)) { 124 | return parse(is); 125 | 126 | } catch (IOException e) { 127 | throw new MailParserException("Error during parsing", e); 128 | } 129 | } 130 | 131 | /** 132 | * Extracts a {@link ParsedEntity} from a mail message.
133 | * ParsedEntity can be: 134 | * 139 | * 140 | * @param eml {@link InputStream} eml mail mime message 141 | * @return {@link ParsedEntity} 142 | */ 143 | public ParsedEntity parse(InputStream eml) { 144 | return parse(MimeMessageUtils.createMimeMessage(eml, properties)); 145 | } 146 | 147 | private static ParsedEntity extract(MimeMessage mimeMessage, boolean isPEC, boolean isPECReceipt, Properties properties, boolean extractAllHeaders) { 148 | 149 | var mail = new Mail(); 150 | 151 | mail.setMessageID(MimeMessageUtils.getMessageID(mimeMessage)); 152 | 153 | var from = MimeMessageUtils.getFrom(mimeMessage); 154 | mail.setFrom(from.stream().filter(r -> r instanceof InternetAddress).map(r -> { 155 | var ia = (InternetAddress) r; 156 | return app.tozzi.model.Address.builder().email(ia.getAddress()).name(ia.getPersonal()).build(); 157 | }).toList()); 158 | 159 | var to = MimeMessageUtils.getTo(mimeMessage); 160 | mail.setTo(to.stream().filter(r -> r instanceof InternetAddress).map(r -> { 161 | var ia = (InternetAddress) r; 162 | return app.tozzi.model.Address.builder().email(ia.getAddress()).name(ia.getPersonal()).build(); 163 | }).toList()); 164 | 165 | var cc = MimeMessageUtils.getCC(mimeMessage); 166 | mail.setCc(cc.stream().filter(r -> r instanceof InternetAddress).map(r -> { 167 | var ia = (InternetAddress) r; 168 | return app.tozzi.model.Address.builder().email(ia.getAddress()).name(ia.getPersonal()).build(); 169 | }).toList()); 170 | 171 | var bcc = MimeMessageUtils.getBCC(mimeMessage); 172 | mail.setBcc(bcc.stream().filter(r -> r instanceof InternetAddress).map(r -> { 173 | var ia = (InternetAddress) r; 174 | return app.tozzi.model.Address.builder().email(ia.getAddress()).name(ia.getPersonal()).build(); 175 | 176 | }).toList()); 177 | 178 | mail.setSentDate(MimeMessageUtils.getSentDate(mimeMessage)); 179 | mail.setReceivedDate(MimeMessageUtils.getReceivedDate(mimeMessage)); 180 | mail.setSubject(MimeMessageUtils.getSubject(mimeMessage)); 181 | 182 | 183 | var inReplyTO = MimeMessageUtils.getHeader(mimeMessage, PECConstants.IN_REPLY_TO); 184 | if (inReplyTO != null) { 185 | mail.setReplyToMessageID(inReplyTO.replaceAll("<", "").replaceAll(">", "")); 186 | } 187 | 188 | var referencesHeader = MimeMessageUtils.getHeaders(mimeMessage, PECConstants.REFERENCES); 189 | if (referencesHeader != null) { 190 | mail.setReplyToHistoryMessagesID(Stream.of(Stream.of(referencesHeader).toList().get(0).split(" ")).map(r -> r.replaceAll("<", "").replaceAll(">", "")).toList()); 191 | } 192 | 193 | if (extractAllHeaders) { 194 | mail.setHeaders(StreamSupport.stream(Spliterators.spliteratorUnknownSize(MimeMessageUtils.getAllHeaders(mimeMessage).asIterator(), 0), false) 195 | .map(e -> Header.builder().key(e.getName()).value(e.getValue()).build()).toList()); 196 | 197 | } 198 | 199 | return extractContent(mail, mimeMessage, isPEC, isPECReceipt, properties, extractAllHeaders); 200 | } 201 | 202 | private static ParsedEntity extractContent(Mail mail, MimeMessage mimeMessage, boolean isPEC, boolean isPECReceipt, Properties properties, boolean extractAllHeaders) { 203 | DataSourcePair dsp = isPEC || isPECReceipt ? new DataSourcePair<>() : null; 204 | extractContent(mail, mimeMessage, isPEC, isPECReceipt, dsp); 205 | 206 | if (isPEC || isPECReceipt) { 207 | 208 | if (isPEC && dsp.getElementA() == null) { 209 | throw new MailParserException("Invalid PEC"); 210 | } 211 | 212 | try { 213 | var postaCertMimeMessage = dsp.getElementA() != null ? MimeMessageUtils.createMimeMessage(dsp.getElementA().getInputStream(), properties) : null; 214 | return PECHandler.loadPEC(postaCertMimeMessage != null ? (Mail) extract(postaCertMimeMessage, false, false, properties, extractAllHeaders) : null, mail, dsp.getElementA(), dsp.getElementB(), mimeMessage); 215 | 216 | } catch (IOException e) { 217 | throw new MailParserException(e); 218 | } 219 | } 220 | 221 | return mail; 222 | } 223 | 224 | private static void extractContent(Mail mail, MimePart part, boolean isPEC, boolean isPECReceipt, DataSourcePair pecAttachments) { 225 | var content = MimeMessageUtils.getContent(part); 226 | 227 | if (MimeMessageUtils.isMimeType(part, MimeTypesUtil.CONTENT_TYPE_TEXT_PLAIN) 228 | && !Part.ATTACHMENT.equalsIgnoreCase(MimeMessageUtils.getDisposition(part)) 229 | && mail.getBodyTXT() == null) { 230 | 231 | var body = content.toString(); 232 | 233 | if (UUEncodingUtils.containsEncodedAttachments(body)) { 234 | body = body.substring(0, UUEncodingUtils.getNextBeginIndex(body)); 235 | mail.getAttachments().addAll(UUEncodingUtils.decodeAttachments(body)); 236 | } 237 | 238 | mail.setBodyTXT(body); 239 | 240 | } else if (MimeMessageUtils.isMimeType(part, MimeTypesUtil.CONTENT_TYPE_TEXT_HTML) 241 | && !Part.ATTACHMENT.equalsIgnoreCase(MimeMessageUtils.getDisposition(part))) { 242 | 243 | mail.setBodyHTML(mail.getBodyHTML() != null ? mail.getBodyHTML() + content.toString() : content.toString()); 244 | 245 | } else if (MimeMessageUtils.isMimeType(part, MimeTypesUtil.CONTENT_TYPE_MULTIPART)) { 246 | 247 | var multipart = (Multipart) content; 248 | for (int i = 0; i < MimeMessageUtils.getCount(multipart); i++) { 249 | extractContent(mail, (MimePart) MimeMessageUtils.getBodyPart(multipart, i), isPEC, isPECReceipt, pecAttachments); 250 | } 251 | 252 | } else if (MimeMessageUtils.isMimeType(part, MimeTypesUtil.CONTENT_TYPE_DELIVERY_STATUS)) { 253 | mail.setHasDeliveryStatus(true); 254 | mail.setDeliveryStatus(DeliveryStatusHandler.loadDeliveryStatus(part)); 255 | 256 | } else { 257 | var pec = false; 258 | 259 | if ((isPEC || isPECReceipt) && !pecAttachments.isComplete()) { 260 | 261 | if (pecAttachments.getElementA() == null 262 | && (PECConstants.POSTACERT_EML_NAME.equalsIgnoreCase(MimeMessageUtils.decodeText(MimeMessageUtils.getFileName(part))) || PECConstants.POSTACERT_EML_NAME.equals(MimeMessageUtils.getFileName(part))) 263 | && MimeMessageUtils.isMimeType(part, MimeTypesUtil.CONTENT_TYPE_MESSAGE_RFC822)) { 264 | 265 | pec = true; 266 | try { 267 | pecAttachments.setElementA(IOUtils.createDataSource(part)); 268 | 269 | } catch (IOException e) { 270 | throw new MailParserException("Error extracting PEC postaCert.eml attachment", e); 271 | } 272 | } else if (pecAttachments.getElementB() == null 273 | && (PECConstants.DATICERT_XML_NAME.equalsIgnoreCase(MimeMessageUtils.decodeText(MimeMessageUtils.getFileName(part))) || PECConstants.DATICERT_XML_NAME.equals(MimeMessageUtils.getFileName(part))) 274 | && MimeMessageUtils.isMimeType(part, MimeTypesUtil.CONTENT_TYPE_APPLICATION_XML)) { 275 | 276 | pec = true; 277 | try { 278 | pecAttachments.setElementB(IOUtils.createDataSource(part)); 279 | } catch (IOException e) { 280 | throw new MailParserException("Error extracting PEC datiCert.xml attachment", e); 281 | } 282 | } 283 | } 284 | 285 | if (!pec) { 286 | DataSource dataSource; 287 | 288 | try { 289 | dataSource = IOUtils.createDataSource(part); 290 | 291 | } catch (IOException e) { 292 | throw new MailParserException("Error extracting attachment", e); 293 | } 294 | 295 | mail.getAttachments().add(Attachment.builder() 296 | .name(dataSource.getName()) 297 | .dataSource(dataSource) 298 | .contentID(MimeMessageUtils.getHeaderValue(MailConstants.CONTENT_ID, part)) 299 | .xAttachmentID(MimeMessageUtils.getHeaderValue(MailConstants.X_ATTACHMENT_ID, part)) 300 | .inline(Part.INLINE.equals(MimeMessageUtils.getDisposition(part))).build() 301 | ); 302 | } 303 | } 304 | } 305 | } -------------------------------------------------------------------------------- /src/main/java/app/tozzi/core/DeliveryStatusHandler.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.core; 2 | 3 | import app.tozzi.model.DeliveryStatus; 4 | import app.tozzi.model.exception.MailParserException; 5 | import app.tozzi.util.MailConstants; 6 | import jakarta.mail.internet.MimePart; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.InputStreamReader; 10 | 11 | /** 12 | * Delivery Status management 13 | * 14 | * @author Biagio Tozzi 15 | */ 16 | public class DeliveryStatusHandler { 17 | 18 | /** 19 | * Extracts a {@link DeliveryStatus} object from {@link MimePart} part 20 | * 21 | * @param part 22 | * @return {@link DeliveryStatus} object with delivery status info 23 | */ 24 | public static DeliveryStatus loadDeliveryStatus(MimePart part) { 25 | 26 | var deliveryStatus = new DeliveryStatus(); 27 | 28 | try (var br = new BufferedReader(new InputStreamReader(part.getInputStream()))) { 29 | 30 | var line = br.readLine(); 31 | 32 | while (line != null) { 33 | 34 | if (line.toLowerCase().startsWith(MailConstants.DELIVERY_ACTION.toLowerCase() + ":")) { 35 | deliveryStatus.setAction(DeliveryStatus.Action.from(line.substring(MailConstants.DELIVERY_ACTION.length() + 1).trim())); 36 | 37 | } else if (line.toLowerCase().startsWith(MailConstants.DELIVERY_STATUS.toLowerCase() + ":")) { 38 | deliveryStatus.setStatus(line.substring(MailConstants.DELIVERY_STATUS.length() + 1).trim()); 39 | 40 | if (!deliveryStatus.getStatus().isEmpty()) { 41 | var prefix = Character.getNumericValue(deliveryStatus.getStatus().charAt(0)); 42 | if (prefix > 0) { 43 | deliveryStatus.setStatusType(DeliveryStatus.StatusType.from(prefix)); 44 | } 45 | } 46 | 47 | } else if (line.toLowerCase().startsWith(MailConstants.DELIVERY_DIAGNOSTIC_CODE.toLowerCase() + ":")) { 48 | var diagnosticCode = line.substring(MailConstants.DELIVERY_DIAGNOSTIC_CODE.length() + 1).trim(); 49 | var dc = new DeliveryStatus.DiagnosticCode(); 50 | if (diagnosticCode.contains(";")) { 51 | dc.setType(diagnosticCode.substring(0, diagnosticCode.indexOf(";")).trim()); 52 | dc.setDescription(diagnosticCode.substring(diagnosticCode.indexOf(";") + 1).trim()); 53 | 54 | } else { 55 | dc.setDescription(diagnosticCode.trim()); 56 | } 57 | 58 | deliveryStatus.setDiagnosticCode(dc); 59 | 60 | } else if (line.toLowerCase().startsWith(MailConstants.DELIVERY_REMOTE_MTA.toLowerCase() + ":")) { 61 | var deliveryRemoteMTA = line.substring(MailConstants.DELIVERY_REMOTE_MTA.length() + 1).trim(); 62 | var drm = new DeliveryStatus.RemoteMTA(); 63 | if (deliveryRemoteMTA.contains(";")) { 64 | drm.setType(deliveryRemoteMTA.substring(0, deliveryRemoteMTA.indexOf(";")).trim()); 65 | drm.setAddress(deliveryRemoteMTA.substring(deliveryRemoteMTA.indexOf(";") + 1).trim()); 66 | } 67 | deliveryStatus.setRemoteMTA(drm); 68 | 69 | } else if (line.toLowerCase().startsWith(MailConstants.DELIVERY_REPORTING_MTA.toLowerCase() + ":")) { 70 | var deliveryRemoteMTA = line.substring(MailConstants.DELIVERY_REPORTING_MTA.length() + 1).trim(); 71 | var drm = new DeliveryStatus.ReportingMTA(); 72 | if (deliveryRemoteMTA.contains(";")) { 73 | drm.setType(deliveryRemoteMTA.substring(0, deliveryRemoteMTA.indexOf(";")).trim()); 74 | drm.setAddress(deliveryRemoteMTA.substring(deliveryRemoteMTA.indexOf(";") + 1).trim()); 75 | } 76 | deliveryStatus.setReportingMTA(drm); 77 | 78 | } else if (line.toLowerCase().startsWith(MailConstants.DELIVERY_RECEIVED_FROM_MTA.toLowerCase() + ":")) { 79 | var deliveryRemoteMTA = line.substring(MailConstants.DELIVERY_RECEIVED_FROM_MTA.length() + 1).trim(); 80 | var drm = new DeliveryStatus.ReceivedFromMTA(); 81 | if (deliveryRemoteMTA.contains(";")) { 82 | drm.setType(deliveryRemoteMTA.substring(0, deliveryRemoteMTA.indexOf(";")).trim()); 83 | drm.setName(deliveryRemoteMTA.substring(deliveryRemoteMTA.indexOf(";") + 1).trim()); 84 | } 85 | deliveryStatus.setReceivedFromMTA(drm); 86 | 87 | } else if (line.toLowerCase().startsWith(MailConstants.DELIVERY_FINAL_RECIPIENT.toLowerCase() + ":")) { 88 | var finalRecipient = line.substring(MailConstants.DELIVERY_FINAL_RECIPIENT.length() + 1).trim(); 89 | var fr = new DeliveryStatus.FinalRecipient(); 90 | if (finalRecipient.contains(";")) { 91 | fr.setType(finalRecipient.substring(0, finalRecipient.indexOf(";")).trim()); 92 | fr.setAddress(finalRecipient.substring(finalRecipient.indexOf(";") + 1).trim()); 93 | } 94 | deliveryStatus.setFinalRecipient(fr); 95 | } 96 | 97 | line = br.readLine(); 98 | } 99 | 100 | 101 | } catch (Exception e) { 102 | throw new MailParserException(e); 103 | } 104 | 105 | return deliveryStatus; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/core/PECHandler.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.core; 2 | 3 | import app.tozzi.model.CertificateData; 4 | import app.tozzi.model.Mail; 5 | import app.tozzi.model.PEC; 6 | import app.tozzi.model.PECReceipt; 7 | import app.tozzi.model.exception.MailParserException; 8 | import app.tozzi.util.MimeMessageUtils; 9 | import app.tozzi.util.PECConstants; 10 | import app.tozzi.util.XMLUtils; 11 | import jakarta.activation.DataSource; 12 | import jakarta.mail.internet.MimeMessage; 13 | import org.xml.sax.SAXException; 14 | 15 | import javax.xml.parsers.DocumentBuilderFactory; 16 | import javax.xml.parsers.ParserConfigurationException; 17 | import javax.xml.xpath.XPathExpressionException; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | 21 | /** 22 | * PEC Management 23 | * 24 | * @author Biagio Tozzi 25 | */ 26 | public class PECHandler { 27 | 28 | private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY; 29 | 30 | static { 31 | DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); 32 | } 33 | 34 | /** 35 | * Extracts PEC receipt from {@link PEC} 36 | * 37 | * @param pec {@link PEC} 38 | * @return {@link PECReceipt} 39 | */ 40 | public static PECReceipt loadReceipt(PEC pec) { 41 | 42 | try { 43 | var pecReceipt = new PECReceipt(); 44 | pecReceipt.setPec(pec); 45 | pecReceipt.setCertificateData(loadCertificateData(pec.getDatiCert().getInputStream())); 46 | return pecReceipt; 47 | 48 | } catch (Exception e) { 49 | throw new MailParserException("Error during PEC receipt processing", e); 50 | } 51 | } 52 | 53 | /** 54 | * Extracts a {@link PEC} 55 | * 56 | * @param originalMessage original mail message 57 | * @param envelope PEC envelope 58 | * @param postaCert postaCert.eml stream 59 | * @param datiCert datiCert.xml stream 60 | * @return {@link PEC} 61 | */ 62 | public static PEC loadPEC(Mail originalMessage, Mail envelope, DataSource postaCert, DataSource datiCert, MimeMessage mimeMessage) { 63 | 64 | try { 65 | var pec = new PEC(); 66 | pec.setPostaCert(postaCert); 67 | pec.setDatiCert(datiCert); 68 | pec.setEnvelope(envelope); 69 | pec.setOriginalMessage(originalMessage); 70 | if (datiCert != null) 71 | pec.setCertificateData(loadCertificateData(datiCert.getInputStream())); 72 | pec.setTransportHeaderValue(MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_TRASPORTO)); 73 | pec.setReceiptHeaderValue(MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_RICEVUTA)); 74 | pec.setReceiptTypeHeaderValue(MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_TIPO_RICEVUTA)); 75 | pec.setSecurityCheckHeaderValue(MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_VERIFICA_SICUREZZA)); 76 | pec.setErrorHeaderValue(MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_TRASPORTO_ERRORE)); 77 | pec.setReferenceHeaderValue(MimeMessageUtils.getHeader(mimeMessage, PECConstants.X_RIFERIMENTO)); 78 | return pec; 79 | 80 | } catch (Exception e) { 81 | throw new MailParserException("Error during PEC processing", e); 82 | } 83 | } 84 | 85 | /** 86 | * Extracts datiCert.xml 87 | * 88 | * @param inputStream datiCert.xml stream 89 | * @return {@link CertificateData} 90 | * @throws ParserConfigurationException 91 | * @throws IOException 92 | * @throws SAXException 93 | * @throws XPathExpressionException 94 | */ 95 | static CertificateData loadCertificateData(InputStream inputStream) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException { 96 | var document = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().parse(inputStream); 97 | var certificateData = new CertificateData(); 98 | 99 | // Sender 100 | var sender = XMLUtils.getTextContent(document, PECConstants.DATICERT_MITTENTE_PATH); 101 | if (sender.isEmpty()) { 102 | throw new MailParserException("Path " + PECConstants.DATICERT_MITTENTE_PATH + " must be not null"); 103 | } 104 | certificateData.setSender(sender.get()); 105 | // Answers 106 | var answers = XMLUtils.getTextContent(document, PECConstants.DATICERT_RISPOSTE_PATH); 107 | if (answers.isEmpty()) { 108 | throw new MailParserException("Path " + PECConstants.DATICERT_RISPOSTE_PATH + " must be not null"); 109 | } 110 | certificateData.setAnswers(answers.get()); 111 | // Answers 112 | var issuer = XMLUtils.getTextContent(document, PECConstants.DATICERT_GESTORE_EMITTENTE_PATH); 113 | if (issuer.isEmpty()) { 114 | throw new MailParserException("Path " + PECConstants.DATICERT_GESTORE_EMITTENTE_PATH + " must be not null"); 115 | } 116 | certificateData.setIssuer(issuer.get()); 117 | // Subject 118 | XMLUtils.getTextContent(document, PECConstants.DATICERT_OGGETTO_PATH).ifPresent(certificateData::setSubject); 119 | // Recipients 120 | var recipients = XMLUtils.getTextAndAttribute(document, PECConstants.DATICERT_DESTINATARI_PATH, PECConstants.DATICERT_DESTINATARI_TIPO_ATTRIBUTE); 121 | if (recipients.isEmpty()) { 122 | throw new MailParserException("Path and attribute " + PECConstants.DATICERT_DESTINATARI_PATH + ";" + PECConstants.DATICERT_DESTINATARI_TIPO_ATTRIBUTE + " must be not null"); 123 | } 124 | certificateData.setRecipients(recipients.entrySet().stream().map(e -> CertificateData.PECRecipients.builder().address(e.getKey()).type(CertificateData.PECRecipients.PECRecipientType.from(e.getValue())).build()).toList()); 125 | // Date, hour and zone 126 | var zone = XMLUtils.getAttribute(document, PECConstants.DATICERT_DATA_PATH, PECConstants.DATICERT_DATA_ZONA_ATTRIBUTE); 127 | if (zone.isEmpty()) { 128 | throw new MailParserException("Path and attribute " + PECConstants.DATICERT_DATA_PATH + ";" + PECConstants.DATICERT_DATA_ZONA_ATTRIBUTE + " must be not null"); 129 | } 130 | certificateData.setDate(new CertificateData.PECDate()); 131 | certificateData.getDate().setZone(zone.get()); 132 | var day = XMLUtils.getTextContent(document, PECConstants.DATICERT_DATA_GIORNO_PATH); 133 | if (day.isEmpty()) { 134 | throw new MailParserException("Path " + PECConstants.DATICERT_DATA_GIORNO_PATH + " must be not null"); 135 | } 136 | certificateData.getDate().setDay(day.get()); 137 | var hour = XMLUtils.getTextContent(document, PECConstants.DATICERT_DATA_ORA_PATH); 138 | if (hour.isEmpty()) { 139 | throw new MailParserException("Path " + PECConstants.DATICERT_DATA_ORA_PATH + " must be not null"); 140 | } 141 | certificateData.getDate().setHour(hour.get()); 142 | // Receipt type 143 | XMLUtils.getAttribute(document, PECConstants.DATICERT_RICEVUTA_PATH, PECConstants.DATICERT_RICEVUTA_TIPO_ATTRIBUTE).ifPresent(recType -> { 144 | certificateData.setReceiptType(CertificateData.ReceiptType.from(recType)); 145 | }); 146 | // Error 147 | var error = XMLUtils.getAttribute(document, PECConstants.DATICERT_POSTACERT_PATH, PECConstants.DATICERT_POSTACERT_ERRORE_ATTRIBUTE); 148 | if (error.isEmpty()) { 149 | throw new MailParserException("Path and attribute " + PECConstants.DATICERT_POSTACERT_PATH + ";" + PECConstants.DATICERT_POSTACERT_ERRORE_ATTRIBUTE + " must be not null"); 150 | } 151 | certificateData.setError(CertificateData.PECError.from(error.get())); 152 | // Extended error 153 | XMLUtils.getTextContent(document, PECConstants.DATICERT_ERRORE_ESTESO_PATH).ifPresent(certificateData::setExtendedError); 154 | // Delivery 155 | XMLUtils.getTextContent(document, PECConstants.DATICERT_CONSEGNA_PATH).ifPresent(certificateData::setDelivery); 156 | // Receiving 157 | XMLUtils.getTextContent(document, PECConstants.DATICERT_RICEZIONE_PATH).ifPresent(certificateData::setReceiving); 158 | // Message ID 159 | XMLUtils.getTextContent(document, PECConstants.DATICERT_MESSAGE_ID_PATH).ifPresent(certificateData::setMessageID); 160 | // ID 161 | XMLUtils.getTextContent(document, PECConstants.DATICERT_IDENTIFICATIVO_PATH).ifPresent(certificateData::setId); 162 | // Type 163 | var type = XMLUtils.getAttribute(document, PECConstants.DATICERT_POSTACERT_PATH, PECConstants.DATICERT_IDENTIFICATIVO_TIPO_ATTRIBUTE); 164 | if (type.isEmpty()) { 165 | throw new MailParserException("Path and attribute " + PECConstants.DATICERT_POSTACERT_PATH + ";" + PECConstants.DATICERT_IDENTIFICATIVO_TIPO_ATTRIBUTE + " must be not null"); 166 | } 167 | certificateData.setType(CertificateData.PostaCertType.from(type.get())); 168 | 169 | return certificateData; 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/Address.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | /** 7 | * Mail/PEC Address 8 | * 9 | * @author Biagio Tozzi 10 | */ 11 | @Data 12 | @Builder 13 | public class Address { 14 | 15 | private String email; 16 | private String name; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/Attachment.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import jakarta.activation.DataSource; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | /** 8 | * Mail/PEC Attachment 9 | * 10 | * @author Biagio Tozzi 11 | */ 12 | @Data 13 | @Builder 14 | public class Attachment { 15 | 16 | private String name; 17 | private DataSource dataSource; 18 | private String contentID; 19 | private String xAttachmentID; 20 | private boolean inline; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/CertificateData.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.Getter; 7 | 8 | import java.time.ZoneId; 9 | import java.time.ZoneOffset; 10 | import java.time.ZonedDateTime; 11 | import java.time.format.DateTimeFormatter; 12 | import java.util.Date; 13 | import java.util.List; 14 | import java.util.stream.Stream; 15 | 16 | /** 17 | * datiCert.xml representation 18 | * 19 | * @author Biagio Tozzi 20 | */ 21 | @Data 22 | public class CertificateData { 23 | 24 | /** 25 | * PEC type from {@value app.tozzi.util.PECConstants#DATICERT_POSTACERT_PATH} and {@value app.tozzi.util.PECConstants#DATICERT_IDENTIFICATIVO_TIPO_ATTRIBUTE} 26 | */ 27 | private PostaCertType type; 28 | 29 | /** 30 | * Sender field from {@value app.tozzi.util.PECConstants#DATICERT_MITTENTE_PATH} 31 | */ 32 | private String sender; 33 | 34 | /** 35 | * Recipients field from {@value app.tozzi.util.PECConstants#DATICERT_DESTINATARI_PATH} and {@value app.tozzi.util.PECConstants#DATICERT_DESTINATARI_TIPO_ATTRIBUTE} 36 | */ 37 | private List recipients; 38 | 39 | /** 40 | * Message ID field from {@value app.tozzi.util.PECConstants#DATICERT_MESSAGE_ID_PATH} 41 | */ 42 | private String messageID; 43 | 44 | /** 45 | * ID field from {@value app.tozzi.util.PECConstants#DATICERT_IDENTIFICATIVO_PATH} 46 | */ 47 | private String id; 48 | 49 | /** 50 | * Error field from {@value app.tozzi.util.PECConstants#DATICERT_POSTACERT_PATH} and {@value app.tozzi.util.PECConstants#DATICERT_POSTACERT_ERRORE_ATTRIBUTE} 51 | */ 52 | private PECError error; 53 | 54 | /** 55 | * Answers field from {@value app.tozzi.util.PECConstants#DATICERT_RISPOSTE_PATH} 56 | */ 57 | private String answers; 58 | 59 | /** 60 | * Subject field from {@value app.tozzi.util.PECConstants#DATICERT_OGGETTO_PATH} 61 | */ 62 | private String subject; 63 | 64 | /** 65 | * Issuer field from {@value app.tozzi.util.PECConstants#DATICERT_GESTORE_EMITTENTE_PATH} 66 | */ 67 | private String issuer; 68 | 69 | /** 70 | * Delivery field from {@value app.tozzi.util.PECConstants#DATICERT_CONSEGNA_PATH} 71 | */ 72 | private String delivery; 73 | 74 | /** 75 | * Receiving field from {@value app.tozzi.util.PECConstants#DATICERT_RICEZIONE_PATH} 76 | */ 77 | private String receiving; 78 | 79 | /** 80 | * Extended error field from {@value app.tozzi.util.PECConstants#DATICERT_ERRORE_ESTESO_PATH} 81 | */ 82 | private String extendedError; 83 | 84 | /** 85 | * Date field from {@value app.tozzi.util.PECConstants#DATICERT_DATA_PATH}, {@value app.tozzi.util.PECConstants#DATICERT_DATA_ZONA_ATTRIBUTE}, {@value app.tozzi.util.PECConstants#DATICERT_DATA_GIORNO_PATH} and {@value app.tozzi.util.PECConstants#DATICERT_DATA_ORA_PATH} 86 | */ 87 | private PECDate date; 88 | 89 | /** 90 | * Receipt type field from {@value app.tozzi.util.PECConstants#DATICERT_RICEVUTA_PATH} and {@value app.tozzi.util.PECConstants#DATICERT_RICEVUTA_TIPO_ATTRIBUTE} 91 | */ 92 | private ReceiptType receiptType; 93 | 94 | @Data 95 | @Builder 96 | public static class PECRecipients { 97 | 98 | private String address; 99 | private PECRecipientType type; 100 | 101 | @AllArgsConstructor 102 | @Getter 103 | public enum PECRecipientType { 104 | ESTERNO("esterno"), // 105 | CERTIFICATO("certificato"); // 106 | 107 | private final String description; 108 | 109 | public static PECRecipientType from(String description) { 110 | return Stream.of(PECRecipientType.values()).filter(t -> t.getDescription().equals(description)).findAny().orElse(null); 111 | } 112 | } 113 | } 114 | 115 | @AllArgsConstructor 116 | public enum PostaCertType { 117 | ACCETTAZIONE("accettazione"), 118 | NON_ACCETTAZIONE("non-accettazione"), 119 | PRESA_IN_CARICO("presa-in-carico"), 120 | AVVENUTA_CONSEGNA("avvenuta-consegna"), 121 | POSTA_CERTIFICATA("posta-certificata"), 122 | ERRORE("errore"), 123 | ERRORE_CONSEGNA("errore-consegna"), 124 | PREAVVISO_ERRORE_CONSEGNA("preavviso-errore-consegna"), 125 | RILEVAZIONE_VIRUS("rilevazione-virus"); 126 | 127 | @Getter 128 | private final String description; 129 | 130 | public static PostaCertType from(String description) { 131 | return Stream.of(PostaCertType.values()).filter(t -> t.getDescription().equals(description)).findAny().orElse(null); 132 | } 133 | } 134 | 135 | @AllArgsConstructor 136 | @Getter 137 | public enum PECError { 138 | NESSUNO("nessuno"), // 139 | NO_DEST("no-dest"), // 140 | NO_DOMINIO("no-dominio"), // 141 | VIRUS("virus"), // 142 | ALTRO("altro"); // 143 | 144 | private final String description; 145 | 146 | public static PECError from(String description) { 147 | return Stream.of(PECError.values()).filter(t -> t.getDescription().equals(description)).findAny().orElse(null); 148 | } 149 | } 150 | 151 | @Data 152 | public static class PECDate { 153 | 154 | private static final String PEC_DATE_PATTERN_1 = "dd/MM/yyyy HH:mm:ss"; 155 | private static final String PEC_DATE_PATTERN_2 = "dd/MM/yyyy"; 156 | 157 | private String zone; 158 | private String day; 159 | private String hour; 160 | 161 | public Date getDate() { 162 | 163 | if (day != null) { 164 | return Date.from(ZonedDateTime.parse(this.day + (this.hour != null ? " " + this.hour : ""), DateTimeFormatter.ofPattern(this.hour != null ? PEC_DATE_PATTERN_1 : PEC_DATE_PATTERN_2).withZone(this.zone != null ? ZoneOffset.of(this.zone) : ZoneId.systemDefault())).toInstant()); 165 | } 166 | 167 | return null; 168 | } 169 | } 170 | 171 | @AllArgsConstructor 172 | public enum ReceiptType { 173 | COMPLETA("completa"), // 174 | BREVE("breve"), // 175 | SINTETICA("sintetica"); // 176 | 177 | @Getter 178 | private final String description; 179 | 180 | public static ReceiptType from(String description) { 181 | return Stream.of(ReceiptType.values()).filter(t -> t.getDescription().equals(description)).findAny().orElse(null); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/DataSourcePair.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @param 7 | * @param 8 | * 9 | * @author Biagio Tozzi 10 | */ 11 | @Data 12 | public class DataSourcePair { 13 | 14 | private A elementA; 15 | private B elementB; 16 | 17 | public boolean isComplete() { 18 | return this.elementA != null && this.elementB != null; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/DeliveryStatus.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | 7 | import java.util.stream.Stream; 8 | 9 | /** 10 | * Delivery Status info 11 | * 12 | * @author Biagio Tozzi 13 | */ 14 | @Data 15 | public class DeliveryStatus { 16 | 17 | private String status; 18 | private DiagnosticCode diagnosticCode; 19 | private Action action; 20 | private ReportingMTA reportingMTA; 21 | private ReceivedFromMTA receivedFromMTA; 22 | private RemoteMTA remoteMTA; 23 | private FinalRecipient finalRecipient; 24 | private StatusType statusType; 25 | 26 | @Data 27 | public static class ReceivedFromMTA { 28 | private String type; 29 | private String name; 30 | } 31 | 32 | @Data 33 | public static class ReportingMTA { 34 | private String type; 35 | private String address; 36 | } 37 | 38 | @Data 39 | public static class RemoteMTA { 40 | private String type; 41 | private String address; 42 | } 43 | 44 | @Data 45 | public static class FinalRecipient { 46 | private String type; 47 | private String address; 48 | } 49 | 50 | @Data 51 | public static class DiagnosticCode { 52 | private String type; 53 | private String description; 54 | } 55 | 56 | @AllArgsConstructor 57 | public enum Action { 58 | FAILED("failed"), FAILURE("failure"), DELAYED("delayed"), DELIVERED("delivered"), RELAYED("relayed"), EXPANDED("expanded"), UNKNOWN("unknown"); 59 | 60 | @Getter 61 | private final String action; 62 | 63 | public static Action from(String action) { 64 | return Stream.of(Action.values()).filter(t -> t.getAction().equals(action)).findAny().orElse(null); 65 | } 66 | } 67 | 68 | @AllArgsConstructor 69 | public enum StatusType { 70 | INFO(2), TEMP_FAILURE(4), PERM_FAILURE(5); 71 | 72 | @Getter 73 | private final int prefix; 74 | 75 | public static StatusType from(int prefix) { 76 | return Stream.of(StatusType.values()).filter(t -> t.getPrefix() == prefix).findAny().orElse(null); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/Header.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | /** 7 | * Mail/PEC header 8 | * 9 | * @author Biagio Tozzi 10 | */ 11 | @Data 12 | @Builder 13 | public class Header { 14 | 15 | private String key; 16 | private String value; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/Mail.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Date; 9 | import java.util.List; 10 | 11 | /** 12 | * Mail 13 | * 14 | * @author Biagio Tozzi 15 | */ 16 | @Data 17 | @EqualsAndHashCode(of = "messageID") 18 | @NoArgsConstructor 19 | public class Mail implements ParsedEntity { 20 | 21 | private String subject; 22 | private List
from; 23 | private List
to; 24 | private List
cc; 25 | private List
bcc; 26 | private String messageID; 27 | private String bodyHTML; 28 | private String bodyTXT; 29 | private Date sentDate; 30 | private Date receivedDate; 31 | 32 | private boolean hasDeliveryStatus; 33 | private DeliveryStatus deliveryStatus; 34 | 35 | private List attachments = new ArrayList<>(); 36 | 37 | private String replyToMessageID; 38 | private List replyToHistoryMessagesID; 39 | 40 | private List
headers; 41 | 42 | @Override 43 | public ParsedEntityType getType() { 44 | return ParsedEntityType.MAIL; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/PEC.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import jakarta.activation.DataSource; 4 | import lombok.Data; 5 | 6 | /** 7 | * PEC 8 | * 9 | * @author Biagio Tozzi 10 | */ 11 | @Data 12 | public class PEC implements ParsedEntity { 13 | 14 | private DataSource datiCert; 15 | private DataSource postaCert; 16 | private Mail envelope; 17 | private Mail originalMessage; 18 | private CertificateData certificateData; 19 | 20 | private String errorHeaderValue; 21 | private String transportHeaderValue; 22 | private String receiptHeaderValue; 23 | private String securityCheckHeaderValue; 24 | private String receiptTypeHeaderValue; 25 | private String referenceHeaderValue; 26 | 27 | @Override 28 | public ParsedEntityType getType() { 29 | return ParsedEntityType.PEC; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/PECReceipt.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | /** 7 | * PEC Receipt 8 | * 9 | * @author Biagio Tozzi 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | public class PECReceipt implements ParsedEntity { 14 | 15 | private PEC pec; 16 | private CertificateData certificateData; 17 | 18 | @Override 19 | public ParsedEntityType getType() { 20 | return ParsedEntityType.PEC_RECEIPT; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/ParsedEntity.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | /** 4 | * @author Biagio Tozzi 5 | */ 6 | public interface ParsedEntity { 7 | 8 | ParsedEntityType getType(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/ParsedEntityType.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model; 2 | 3 | /** 4 | * Type of parsed entity 5 | * 6 | * @author Biagio Tozzi 7 | */ 8 | public enum ParsedEntityType { 9 | 10 | MAIL, PEC, PEC_RECEIPT; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/model/exception/MailParserException.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.model.exception; 2 | 3 | /** 4 | * @author Biagio Tozzi 5 | */ 6 | public class MailParserException extends RuntimeException { 7 | 8 | public MailParserException(String message, Throwable cause) { 9 | super(message, cause); 10 | } 11 | 12 | public MailParserException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public MailParserException(String message) { 17 | super(message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/util/IOUtils.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import jakarta.activation.DataSource; 4 | import jakarta.activation.FileTypeMap; 5 | import jakarta.mail.Part; 6 | import jakarta.mail.internet.MimePart; 7 | import jakarta.mail.util.ByteArrayDataSource; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.nio.Buffer; 14 | import java.nio.ByteBuffer; 15 | import java.nio.channels.Channels; 16 | import java.nio.channels.ReadableByteChannel; 17 | import java.nio.channels.WritableByteChannel; 18 | 19 | /** 20 | * IO Utilities 21 | * 22 | * @author Biagio Tozzi 23 | */ 24 | public class IOUtils { 25 | 26 | /** 27 | * Creates a {@link DataSource} from {@link MimePart} 28 | * 29 | * @param part {@link MimePart} 30 | * @return {@link DataSource} 31 | * @throws IOException 32 | */ 33 | public static DataSource createDataSource(MimePart part) throws IOException { 34 | return createDataSource(part, null); 35 | } 36 | 37 | /** 38 | * Creates a {@link DataSource} from {@link InputStream} and file name 39 | * 40 | * @param inputStream {@link InputStream} 41 | * @param name File name 42 | * @return {@link DataSource} 43 | * @throws IOException 44 | */ 45 | public static DataSource createDataSource(InputStream inputStream, String name) throws IOException { 46 | byte[] content = getContent(inputStream); 47 | name = name == null ? "unknown_name" : name; 48 | ByteArrayDataSource result = new ByteArrayDataSource(content, getFileMimeType(name)); 49 | result.setName(name); 50 | return result; 51 | } 52 | 53 | /** 54 | * Creates a {@link DataSource} from {@link MimePart} and file name 55 | * 56 | * @param part {@link MimePart} 57 | * @param name File name 58 | * @return {@link DataSource} 59 | * @throws IOException 60 | */ 61 | public static DataSource createDataSource(MimePart part, String name) throws IOException { 62 | var dataSource = MimeMessageUtils.getDataHandler(part).getDataSource(); 63 | byte[] content = getContent(dataSource.getInputStream()); 64 | var fileName = name != null ? name : loadNameForDataSource(part); 65 | var result = new ByteArrayDataSource(content, getBaseMimeType(dataSource, fileName)); 66 | result.setName(fileName); 67 | return result; 68 | } 69 | 70 | /** 71 | * Copy {@link InputStream} into {@link OutputStream} with buffer of 16KB 72 | * 73 | * @param src {@link InputStream} 74 | * @param dest {@link OutputStream} 75 | * @throws IOException 76 | */ 77 | public static void fastCopy(InputStream src, OutputStream dest) throws IOException { 78 | var inputChannel = Channels.newChannel(src); 79 | var outputChannel = Channels.newChannel(dest); 80 | fastCopy(inputChannel, outputChannel); 81 | inputChannel.close(); 82 | outputChannel.close(); 83 | } 84 | 85 | private static String loadNameForDataSource(MimePart part) { 86 | var fileName = MimeMessageUtils.getFileName(part); 87 | return fileName != null && !fileName.trim().isEmpty() ? MimeMessageUtils.decodeText(fileName) : getName(part); 88 | } 89 | 90 | static String getName(Part part) { 91 | 92 | var fileName = MimeMessageUtils.getFileName(part); 93 | 94 | if (fileName != null && !fileName.trim().isEmpty()) 95 | return MimeMessageUtils.decodeText(fileName); 96 | 97 | var extension = getExtension(part); 98 | extension = extension != null && !extension.trim().isEmpty() ? ("." + extension) : ""; 99 | 100 | if (Part.INLINE.equals(MimeMessageUtils.getDisposition(part))) { 101 | return "unknown_name_inline" + extension; 102 | } 103 | 104 | return "unknown_name" + extension; 105 | } 106 | 107 | private static String getExtension(Part part) { 108 | 109 | var fullMimeType = MimeMessageUtils.getContentType(part); 110 | 111 | if (fullMimeType != null && !fullMimeType.trim().isEmpty()) { 112 | return MimeTypesUtil.guessExtension(fullMimeType); 113 | } 114 | 115 | return null; 116 | } 117 | 118 | private static String getBaseMimeType(DataSource dataSource, String fileName) { 119 | var fullMimeType = dataSource.getContentType(); 120 | 121 | if (fullMimeType == null || fullMimeType.trim().isEmpty()) { 122 | return getFileMimeType(fileName); 123 | } 124 | 125 | var pos = fullMimeType.indexOf(';'); 126 | if (pos >= 0) { 127 | return fullMimeType.substring(0, pos); 128 | } 129 | return fullMimeType; 130 | } 131 | 132 | static byte[] getContent(InputStream is) throws IOException { 133 | byte[] result; 134 | var os = new ByteArrayOutputStream(); 135 | fastCopy(is, os); 136 | result = os.toByteArray(); 137 | return result; 138 | } 139 | 140 | private static void fastCopy(ReadableByteChannel src, WritableByteChannel dest) throws IOException { 141 | var buffer = ByteBuffer.allocateDirect(16 * 1024); 142 | 143 | while (src.read(buffer) != -1) { 144 | ((Buffer) buffer).flip(); 145 | dest.write(buffer); 146 | buffer.compact(); 147 | } 148 | 149 | ((Buffer) buffer).flip(); 150 | 151 | while (buffer.hasRemaining()) { 152 | dest.write(buffer); 153 | } 154 | } 155 | 156 | static String getFileMimeType(String fileName) { 157 | 158 | String result; 159 | var dotPos = fileName.lastIndexOf("."); 160 | 161 | if (dotPos < 0) { 162 | result = MimeTypesUtil.CONTENT_TYPE_OCTETSTREAM; 163 | 164 | } else { 165 | var fileExt = fileName.substring(dotPos + 1); 166 | 167 | if (fileExt.isEmpty()) { 168 | result = MimeTypesUtil.CONTENT_TYPE_OCTETSTREAM; 169 | 170 | } else { 171 | result = MimeTypesUtil.guessMimeType(fileExt); 172 | } 173 | } 174 | 175 | if (MimeTypesUtil.CONTENT_TYPE_OCTETSTREAM.equals(result)) { 176 | result = FileTypeMap.getDefaultFileTypeMap().getContentType(fileName); 177 | } 178 | 179 | return result != null ? result : MimeTypesUtil.CONTENT_TYPE_OCTETSTREAM; 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/util/MailConstants.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | /** 4 | * @author Biagio Tozzi 5 | */ 6 | public class MailConstants { 7 | 8 | public static final String DELIVERY_ACTION = "action"; 9 | public static final String DELIVERY_STATUS = "status"; 10 | public static final String DELIVERY_DIAGNOSTIC_CODE = "Diagnostic-Code"; 11 | public static final String DELIVERY_REMOTE_MTA = "Remote-MTA"; 12 | public static final String DELIVERY_REPORTING_MTA = "Reporting-MTA"; 13 | public static final String DELIVERY_RECEIVED_FROM_MTA = "Received-From-MTA"; 14 | public static final String DELIVERY_FINAL_RECIPIENT = "Final-Recipient"; 15 | 16 | public static final String X_ATTACHMENT_ID = "X-Attachment-Id"; 17 | public static final String CONTENT_ID = "Content-ID"; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/util/MimeMessageUtils.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import app.tozzi.model.exception.MailParserException; 4 | import jakarta.activation.DataHandler; 5 | import jakarta.mail.*; 6 | import jakarta.mail.internet.InternetAddress; 7 | import jakarta.mail.internet.MimeMessage; 8 | import jakarta.mail.internet.MimeUtility; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.UnsupportedEncodingException; 14 | import java.nio.charset.StandardCharsets; 15 | import java.security.MessageDigest; 16 | import java.security.NoSuchAlgorithmException; 17 | import java.sql.Timestamp; 18 | import java.text.DateFormat; 19 | import java.text.SimpleDateFormat; 20 | import java.time.ZoneId; 21 | import java.time.ZonedDateTime; 22 | import java.util.*; 23 | import java.util.stream.Stream; 24 | 25 | /** 26 | * MIME Utilities 27 | * 28 | * @author Biagio Tozzi 29 | */ 30 | @Slf4j 31 | public class MimeMessageUtils { 32 | 33 | private static final String DATE_FORMAT = "YYYYMMDDHHMMSS"; 34 | private static final ZoneId ZI = ZoneId.of("Europe/Rome"); 35 | 36 | /** 37 | * Extracts message ID from {@link MimeMessage} 38 | * 39 | * @param mimeMessage {@link MimeMessage} 40 | * @return message ID 41 | */ 42 | public static String getMessageID(MimeMessage mimeMessage) { 43 | 44 | try { 45 | var messageID = mimeMessage.getMessageID(); 46 | 47 | if (messageID != null && !messageID.isBlank()) { 48 | return messageID.replaceAll("<", "").replaceAll(">", ""); 49 | } 50 | 51 | return getUniqueMessageID(mimeMessage); 52 | 53 | } catch (MessagingException | NoSuchAlgorithmException e) { 54 | throw new MailParserException("Error reading messageID", e); 55 | } 56 | 57 | } 58 | 59 | /** 60 | * Extracts subject from {@link MimeMessage} 61 | * 62 | * @param mimeMessage {@link MimeMessage} 63 | * @return subject 64 | */ 65 | public static String getSubject(MimeMessage mimeMessage) { 66 | 67 | try { 68 | return mimeMessage.getSubject(); 69 | 70 | } catch (MessagingException e) { 71 | throw new MailParserException("Error reading subject", e); 72 | } 73 | 74 | } 75 | 76 | /** 77 | * Extracts received date from {@link MimeMessage} 78 | * 79 | * @param mimeMessage {@link MimeMessage} 80 | * @return received date 81 | */ 82 | public static Date getReceivedDate(MimeMessage mimeMessage) { 83 | 84 | try { 85 | return mimeMessage.getReceivedDate(); 86 | 87 | } catch (MessagingException e) { 88 | throw new MailParserException("Error reading received date", e); 89 | } 90 | 91 | } 92 | 93 | /** 94 | * Extracts sent date from {@link MimeMessage} 95 | * 96 | * @param mimeMessage {@link MimeMessage} 97 | * @return sent date 98 | */ 99 | public static Date getSentDate(MimeMessage mimeMessage) { 100 | 101 | try { 102 | return mimeMessage.getSentDate(); 103 | 104 | } catch (MessagingException e) { 105 | throw new MailParserException("Error reading sent date", e); 106 | } 107 | 108 | } 109 | 110 | /** 111 | * Extracts BCC recipients from {@link MimeMessage} 112 | * 113 | * @param mimeMessage {@link MimeMessage} 114 | * @return BCC Recipients 115 | */ 116 | public static List
getBCC(MimeMessage mimeMessage) { 117 | 118 | try { 119 | return Stream.ofNullable(mimeMessage.getRecipients(Message.RecipientType.BCC)).flatMap(Stream::of).toList(); 120 | 121 | } catch (MessagingException e) { 122 | throw new MailParserException("Error reading BCC recipients", e); 123 | } 124 | 125 | } 126 | 127 | /** 128 | * Extracts CC recipients from {@link MimeMessage} 129 | * 130 | * @param mimeMessage {@link MimeMessage} 131 | * @return CC Recipients 132 | */ 133 | public static List
getCC(MimeMessage mimeMessage) { 134 | 135 | try { 136 | return Stream.ofNullable(mimeMessage.getRecipients(Message.RecipientType.CC)).flatMap(Stream::of).toList(); 137 | 138 | } catch (MessagingException e) { 139 | throw new MailParserException("Error reading CC recipients", e); 140 | } 141 | 142 | } 143 | 144 | /** 145 | * Extracts TO recipients from {@link MimeMessage} 146 | * 147 | * @param mimeMessage {@link MimeMessage} 148 | * @return TO Recipients 149 | */ 150 | public static List
getTo(MimeMessage mimeMessage) { 151 | 152 | try { 153 | return Stream.ofNullable(mimeMessage.getRecipients(Message.RecipientType.TO)).flatMap(Stream::of).toList(); 154 | 155 | } catch (MessagingException e) { 156 | throw new MailParserException("Error reading TO recipients", e); 157 | } 158 | 159 | } 160 | 161 | /** 162 | * Extracts from address from {@link MimeMessage} 163 | * 164 | * @param mimeMessage {@link MimeMessage} 165 | * @return from address 166 | */ 167 | public static List
getFrom(MimeMessage mimeMessage) { 168 | 169 | try { 170 | return Stream.ofNullable(mimeMessage.getFrom()).flatMap(Stream::of).toList(); 171 | 172 | } catch (MessagingException e) { 173 | throw new MailParserException("Error reading sender", e); 174 | } 175 | 176 | } 177 | 178 | /** 179 | * Extracts a {@link MimeMessage} single header value from key headerKey 180 | * 181 | * @param mimeMessage {@link MimeMessage} 182 | * @param headerKey Header key 183 | * @return Header value 184 | */ 185 | public static String getHeader(MimeMessage mimeMessage, String headerKey) { 186 | 187 | try { 188 | return mimeMessage.getHeader(headerKey, ","); 189 | 190 | } catch (MessagingException e) { 191 | throw new MailParserException("Error reading header " + headerKey, e); 192 | } 193 | } 194 | 195 | /** 196 | * Extracts {@link MimeMessage} header values from key headerKey 197 | * 198 | * @param mimeMessage {@link MimeMessage} 199 | * @param headerKey Header key 200 | * @return Header values 201 | */ 202 | public static String[] getHeaders(MimeMessage mimeMessage, String headerKey) { 203 | 204 | try { 205 | return mimeMessage.getHeader(headerKey); 206 | 207 | } catch (MessagingException e) { 208 | throw new MailParserException("Error reading header " + headerKey, e); 209 | } 210 | } 211 | 212 | /** 213 | * Extracts a single {@link Part} header value of key headerKey 214 | * 215 | * @param part {@link Part} 216 | * @param headerKey Header key 217 | * @return Header value 218 | */ 219 | public static String getHeaderValue(String headerKey, Part part) { 220 | var values = MimeMessageUtils.getHeaderValues(headerKey, part); 221 | return values == null || values.isEmpty() ? null : values.get(0); 222 | } 223 | 224 | /** 225 | * Extracts {@link Part} header values of key headerKey 226 | * 227 | * @param part {@link Part} 228 | * @param headerKey Header key 229 | * @return Header values 230 | */ 231 | public static List getHeaderValues(String headerKey, Part part) { 232 | 233 | try { 234 | String[] res = part.getHeader(headerKey); 235 | 236 | if (res != null) { 237 | return Stream.of(res).toList(); 238 | } 239 | 240 | return null; 241 | 242 | } catch (MessagingException e) { 243 | throw new MailParserException("Error reading header " + headerKey + " of part: " + getDescription(part), e); 244 | } 245 | 246 | } 247 | 248 | /** 249 | * Retrieve content type of {@link Part} 250 | * 251 | * @param part {@link Part} 252 | * @return Content Type 253 | */ 254 | public static String getContentType(Part part) { 255 | 256 | try { 257 | return part.getContentType(); 258 | 259 | } catch (MessagingException e) { 260 | throw new MailParserException("Error reading content type. Part: " + getDescription(part), e); 261 | } 262 | 263 | } 264 | 265 | /** 266 | * Retrieve disposition of {@link Part} 267 | * 268 | * @param part {@link Part} 269 | * @return disposition 270 | */ 271 | public static String getDisposition(Part part) { 272 | 273 | try { 274 | return part.getDisposition(); 275 | 276 | } catch (MessagingException e) { 277 | throw new MailParserException("Error reading disposition. Part: " + getDescription(part), e); 278 | } 279 | } 280 | 281 | /** 282 | * Retrieve count of {@link Multipart} 283 | * 284 | * @param multiPart {@link Multipart} 285 | * @return count 286 | */ 287 | public static int getCount(Multipart multiPart) { 288 | 289 | try { 290 | return multiPart.getCount(); 291 | 292 | } catch (MessagingException e) { 293 | throw new MailParserException("Error during counting parts", e); 294 | } 295 | } 296 | 297 | /** 298 | * Retrieve file name of {@link Part} 299 | * 300 | * @param part {@link Part} 301 | * @return file name 302 | */ 303 | public static String getFileName(Part part) { 304 | try { 305 | return part.getFileName(); 306 | 307 | } catch (MessagingException e) { 308 | throw new MailParserException("Error reading file name. Part: " + getDescription(part), e); 309 | } 310 | } 311 | 312 | /** 313 | * Extracts index-th {@link BodyPart} of {@link Multipart} part 314 | * 315 | * @param multiPart {@link Multipart} 316 | * @param index part index 317 | * @return {@link BodyPart} 318 | */ 319 | public static BodyPart getBodyPart(Multipart multiPart, int index) { 320 | 321 | try { 322 | return multiPart.getBodyPart(index); 323 | 324 | } catch (MessagingException e) { 325 | throw new MailParserException("Error reading body part n. " + index, e); 326 | } 327 | 328 | } 329 | 330 | private static String getDescription(Part part) { 331 | 332 | try { 333 | return part.getDescription(); 334 | 335 | } catch (MessagingException e) { 336 | log.error("Error reading part description", e); 337 | return null; 338 | } 339 | } 340 | 341 | /** 342 | * Extracts content of {@link Part} 343 | * 344 | * @param part {@link Part} 345 | * @return content 346 | */ 347 | public static Object getContent(Part part) { 348 | 349 | try { 350 | return part.getContent(); 351 | 352 | } catch (IOException | MessagingException e) { 353 | throw new MailParserException("Error reading content", e); 354 | } 355 | 356 | } 357 | 358 | /** 359 | * Check that the {@link Part} is of the mimeType 360 | * 361 | * @param part {@link Part} 362 | * @param mimeType Mime type 363 | * @return true/false 364 | */ 365 | public static boolean isMimeType(Part part, String mimeType) { 366 | 367 | try { 368 | return part.isMimeType(mimeType); 369 | 370 | } catch (MessagingException e) { 371 | throw new MailParserException("Error reading mime type of part " + getDescription(part), e); 372 | } 373 | } 374 | 375 | /** 376 | * Extracts {@link DataHandler} from {@link Part} 377 | * 378 | * @param part {@link Part} 379 | * @return {@link DataHandler} 380 | */ 381 | public static DataHandler getDataHandler(Part part) { 382 | 383 | try { 384 | return part.getDataHandler(); 385 | 386 | } catch (MessagingException e) { 387 | throw new MailParserException("Error reading data handler of part: " + getDescription(part), e); 388 | } 389 | } 390 | 391 | /** 392 | * Decode text 393 | * 394 | * @param text 395 | * @return decoded text 396 | */ 397 | public static String decodeText(String text) { 398 | 399 | try { 400 | return MimeUtility.decodeText(text); 401 | 402 | } catch (UnsupportedEncodingException e) { 403 | throw new MailParserException("Error decoding text " + text, e); 404 | } 405 | } 406 | 407 | /** 408 | * Decode stream {@link InputStream} type encoded 409 | * 410 | * @param inputStream {@link InputStream} 411 | * @param type encoding type 412 | * @return Decoded stream 413 | */ 414 | public static InputStream decodeStream(InputStream inputStream, String type) { 415 | 416 | try { 417 | return MimeUtility.decode(inputStream, type); 418 | 419 | } catch (MessagingException e) { 420 | throw new MailParserException("Error decoding stream", e); 421 | } 422 | } 423 | 424 | /** 425 | * Creates {@link MimeMessage} from {@link InputStream} 426 | * 427 | * @param inputStream {@link InputStream} 428 | * @param properties {@link Properties} 429 | * @return MIME Message 430 | */ 431 | public static MimeMessage createMimeMessage(InputStream inputStream, Properties properties) { 432 | 433 | try { 434 | return new MimeMessage(Session.getDefaultInstance(properties != null ? properties : System.getProperties()), inputStream); 435 | 436 | } catch (MessagingException e) { 437 | throw new MailParserException("Error creating mime message", e); 438 | } 439 | } 440 | 441 | /** 442 | * Extracts all headers from {@link MimeMessage} 443 | * 444 | * @param mimeMessage {@link MimeMessage} 445 | * @return all headers (key, values) 446 | */ 447 | public static Enumeration
getAllHeaders(MimeMessage mimeMessage) { 448 | try { 449 | return mimeMessage.getAllHeaders(); 450 | 451 | } catch (MessagingException e) { 452 | throw new MailParserException("Error reading all headers", e); 453 | } 454 | } 455 | 456 | private static String getUniqueMessageID(MimeMessage mimeMessage) throws NoSuchAlgorithmException, MessagingException { 457 | 458 | var res = new StringBuilder(); 459 | DateFormat df = new SimpleDateFormat(DATE_FORMAT); 460 | 461 | boolean sd = false; 462 | boolean rd = false; 463 | 464 | if (mimeMessage.getSentDate() != null) { 465 | res.append(df.format(mimeMessage.getSentDate())).append("_"); 466 | sd = true; 467 | } 468 | 469 | if (mimeMessage.getReceivedDate() != null) { 470 | res.append(df.format(mimeMessage.getReceivedDate())).append("_"); 471 | rd = true; 472 | } 473 | 474 | if (res.isEmpty() || (!rd && !sd)) { 475 | return Timestamp.from(ZonedDateTime.now(ZI).toInstant()).hashCode() + UUID.randomUUID().toString(); 476 | } 477 | 478 | 479 | if (mimeMessage.getSender() != null && mimeMessage.getSender() instanceof InternetAddress ia) { 480 | res.append(ia.getAddress()).append("_"); 481 | } 482 | 483 | if (mimeMessage.getAllRecipients() != null) { 484 | List rec = Stream.of(mimeMessage.getAllRecipients()).filter(r -> r instanceof InternetAddress).map(r -> ((InternetAddress) r).getAddress()).toList(); 485 | if (!rec.isEmpty()) { 486 | res.append(String.join("_", rec)); 487 | } 488 | } 489 | 490 | return new String(MessageDigest.getInstance("SHA-256").digest(res.toString().getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); 491 | 492 | } 493 | 494 | } 495 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/util/PECConstants.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | /** 4 | * @author Biagio Tozzi 5 | */ 6 | public class PECConstants { 7 | 8 | public static final String X_TRASPORTO = "X-Trasporto"; 9 | public static final String X_RICEVUTA = "X-Ricevuta"; 10 | public static final String X_RIFERIMENTO = "X-Riferimento-Message-ID"; 11 | public static final String X_TIPO_RICEVUTA = "X-TipoRicevuta"; 12 | public static final String X_VERIFICA_SICUREZZA = "X-VerificaSicurezza"; 13 | public static final String X_TRASPORTO_ERRORE = "errore"; 14 | 15 | public static final String POSTACERT_EML_NAME = "postacert.eml"; 16 | public static final String DATICERT_XML_NAME = "daticert.xml"; 17 | 18 | public static final String IN_REPLY_TO = "In-Reply-To"; 19 | public static final String REFERENCES = "References"; 20 | 21 | public static final String DATICERT_MITTENTE_PATH = "/postacert/intestazione/mittente"; 22 | public static final String DATICERT_DESTINATARI_PATH = "/postacert/intestazione/destinatari"; 23 | public static final String DATICERT_DESTINATARI_TIPO_ATTRIBUTE = "tipo"; 24 | public static final String DATICERT_RISPOSTE_PATH = "/postacert/intestazione/risposte"; 25 | public static final String DATICERT_OGGETTO_PATH = "/postacert/intestazione/oggetto"; 26 | public static final String DATICERT_GESTORE_EMITTENTE_PATH = "/postacert/dati/gestore-emittente"; 27 | public static final String DATICERT_DATA_PATH = "/postacert/dati/data"; 28 | public static final String DATICERT_DATA_ZONA_ATTRIBUTE = "zona"; 29 | public static final String DATICERT_DATA_GIORNO_PATH = "/postacert/dati/data/giorno"; 30 | public static final String DATICERT_DATA_ORA_PATH = "/postacert/dati/data/ora"; 31 | public static final String DATICERT_RICEVUTA_PATH = "/postacert/dati/ricevuta"; 32 | public static final String DATICERT_RICEVUTA_TIPO_ATTRIBUTE = "tipo"; 33 | public static final String DATICERT_POSTACERT_PATH = "/postacert"; 34 | public static final String DATICERT_POSTACERT_ERRORE_ATTRIBUTE = "errore"; 35 | public static final String DATICERT_ERRORE_ESTESO_PATH = "/postacert/dati/errore-esteso"; 36 | public static final String DATICERT_CONSEGNA_PATH = "/postacert/dati/consegna"; 37 | public static final String DATICERT_RICEZIONE_PATH = "/postacert/dati/ricezione"; 38 | public static final String DATICERT_MESSAGE_ID_PATH = "/postacert/dati/msgid"; 39 | public static final String DATICERT_IDENTIFICATIVO_PATH = "/postacert/dati/identificativo"; 40 | public static final String DATICERT_IDENTIFICATIVO_TIPO_ATTRIBUTE = "tipo"; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/util/UUEncodingUtils.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import app.tozzi.model.Attachment; 4 | import app.tozzi.model.exception.MailParserException; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.io.*; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * UUEncoding/UUDecoding management 13 | * 14 | * @author Biagio Tozzi 15 | */ 16 | @Slf4j 17 | public class UUEncodingUtils { 18 | 19 | /** 20 | * Checks if the content is encoded with uuencode encoding 21 | * 22 | * @param content content 23 | * @return true/false 24 | */ 25 | public static boolean containsEncodedAttachments(String content) { 26 | 27 | if (content == null) 28 | return false; 29 | 30 | var beginIndex = content.indexOf("begin "); 31 | var endIndex = content.indexOf("end"); 32 | 33 | if (beginIndex == -1 || endIndex == -1) 34 | return false; 35 | 36 | if (beginIndex > endIndex) { 37 | return containsEncodedAttachments(content.substring(beginIndex)); 38 | } 39 | 40 | var subString = content.substring(beginIndex, endIndex + 3); 41 | 42 | if (!internalContainsEncodedAttachments(subString)) { 43 | return containsEncodedAttachments(content.substring(beginIndex + 6)); 44 | } 45 | 46 | return true; 47 | } 48 | 49 | /** 50 | * Decode and extracts uuencoded attachments 51 | * 52 | * @param content content 53 | * @return attachments 54 | */ 55 | public static List decodeAttachments(String content) { 56 | var result = new ArrayList(); 57 | 58 | try { 59 | internalDecodeAttachments(content, result); 60 | 61 | } catch (IOException e) { 62 | throw new MailParserException(e); 63 | } 64 | 65 | return result; 66 | } 67 | 68 | /** 69 | * Calculates uuencoding begin index 70 | * 71 | * @param content content 72 | * @return index 73 | */ 74 | public static int getNextBeginIndex(String content) { 75 | 76 | if (content == null) 77 | return -1; 78 | 79 | var beginIndex = content.indexOf("begin "); 80 | var endIndex = content.indexOf("end"); 81 | 82 | if (beginIndex == -1 || endIndex == -1) 83 | return -1; 84 | 85 | if (beginIndex > endIndex) { 86 | return getNextBeginIndex(content.substring(endIndex + 3)); 87 | } 88 | 89 | var subString = content.substring(beginIndex, endIndex + 3); 90 | 91 | if (internalContainsEncodedAttachments(subString)) { 92 | return beginIndex; 93 | } 94 | 95 | return getNextBeginIndex(content.substring(beginIndex + 6)); 96 | } 97 | 98 | private static void internalDecodeAttachments(String content, List attachments) throws IOException { 99 | 100 | if (content == null) 101 | return; 102 | 103 | var beginIndex = content.indexOf("begin "); 104 | var endIndex = content.indexOf("end"); 105 | 106 | if (beginIndex == -1 || endIndex == -1) 107 | return; 108 | 109 | if (beginIndex > endIndex) { 110 | internalDecodeAttachments(((content.substring(beginIndex))), attachments); 111 | 112 | } else { 113 | var subString = content.substring(beginIndex, endIndex + 3); 114 | 115 | if (internalContainsEncodedAttachments(subString)) { 116 | InputStream isDecoded = null; 117 | String fileName = null; 118 | 119 | try (var is = new ByteArrayInputStream(content.getBytes())) { 120 | fileName = getAttachmentName(subString, attachments); 121 | isDecoded = MimeMessageUtils.decodeStream(is, "uuencode"); 122 | attachments.add(Attachment.builder().name(fileName).dataSource(IOUtils.createDataSource(isDecoded, fileName)).build()); 123 | } 124 | } 125 | 126 | internalDecodeAttachments(((content.substring(beginIndex + 6))), attachments); 127 | } 128 | } 129 | 130 | private static boolean internalContainsEncodedAttachments(String content) { 131 | 132 | try (var reader = new BufferedReader(new StringReader(content))) { 133 | var firstLine = reader.readLine(); 134 | if (firstLine == null || !firstLine.regionMatches(false, 0, "begin ", 0, 6)) 135 | return false; 136 | 137 | var lastLine = reader.lines().reduce((first, second) -> second).orElse(null); 138 | if (lastLine == null || !lastLine.regionMatches(false, 0, "end", 0, 3)) 139 | return false; 140 | 141 | int mode; 142 | 143 | try { 144 | mode = Integer.parseInt(firstLine.substring(6, 9)); 145 | 146 | } catch (NumberFormatException e) { 147 | log.warn("Permissions mode not valid", e); 148 | return false; 149 | } 150 | 151 | if (!isOctal(mode)) { 152 | log.warn("Permissions mode not in octal format"); 153 | return false; 154 | } 155 | 156 | String fileName = firstLine.substring(9); 157 | if (fileName.isBlank()) { 158 | log.warn("File name not present"); 159 | return false; 160 | } 161 | 162 | return true; 163 | 164 | } catch (Exception e) { 165 | log.error("Error during uuencoded content check", e); 166 | throw new MailParserException(e); 167 | } 168 | } 169 | 170 | private static String getAttachmentName(String content, List attachments) { 171 | 172 | try (var reader = new BufferedReader(new StringReader(content))) { 173 | return reader.readLine().substring(9).trim(); 174 | 175 | } catch (Exception e) { 176 | log.error("Error during reading file name", e); 177 | return getVersionedFileName("unnamed", attachments.stream().map(Attachment::getName).toList(), 1); 178 | 179 | } 180 | } 181 | 182 | private static String getVersionedFileName(String fileName, List files, int i) { 183 | 184 | if (files.contains(fileName)) { 185 | fileName = fileName + "(" + i + ")"; 186 | return getVersionedFileName(fileName, files, i++); 187 | } 188 | 189 | return fileName; 190 | } 191 | 192 | private static boolean isOctal(int number) { 193 | var isOctal = false; 194 | 195 | while (number > 0) { 196 | if (number % 10 <= 7) { 197 | isOctal = true; 198 | } else { 199 | isOctal = false; 200 | break; 201 | } 202 | number /= 10; 203 | 204 | } 205 | 206 | return isOctal; 207 | } 208 | 209 | 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/app/tozzi/util/XMLUtils.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import org.w3c.dom.Document; 4 | import org.w3c.dom.Node; 5 | import org.w3c.dom.NodeList; 6 | 7 | import javax.xml.xpath.XPathConstants; 8 | import javax.xml.xpath.XPathExpressionException; 9 | import javax.xml.xpath.XPathFactory; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Optional; 14 | import java.util.stream.IntStream; 15 | 16 | /** 17 | * @author Biagio Tozzi 18 | */ 19 | public class XMLUtils { 20 | 21 | /** 22 | * Extracts attribute value from {@link Document} 23 | * 24 | * @param doc document 25 | * @param path path 26 | * @param attributeName attribute 27 | * @return attribute value 28 | * @throws XPathExpressionException 29 | */ 30 | public static Optional getAttribute(Document doc, String path, String attributeName) throws XPathExpressionException { 31 | 32 | return getNodes(doc, path).stream() 33 | .findFirst() 34 | .flatMap(node -> { 35 | var attributes = node.getAttributes(); 36 | for (var i = 0; i < attributes.getLength(); i++) { 37 | if (attributes.item(i).getNodeName().equalsIgnoreCase(attributeName)) { 38 | return Optional.ofNullable(attributes.item(i).getNodeValue()); 39 | } 40 | } 41 | return Optional.empty(); 42 | }) 43 | .or(Optional::empty); 44 | } 45 | 46 | /** 47 | * Extracts text content and node value of attribute from {@link Document} 48 | * 49 | * @param document document 50 | * @param path path 51 | * @param attribute attribute 52 | * @return text and attribute 53 | * @throws XPathExpressionException 54 | */ 55 | public static Map getTextAndAttribute(Document document, String path, String attribute) throws XPathExpressionException { 56 | var result = new HashMap(); 57 | var nodes = getNodes(document, path); 58 | 59 | nodes.forEach(node -> { 60 | var value = node.getTextContent(); 61 | String attr = null; 62 | 63 | var attributes = node.getAttributes(); 64 | for (int j = 0; j < attributes.getLength(); j++) { 65 | if (attributes.item(j).getNodeName().equalsIgnoreCase(attribute)) { 66 | attr = attributes.item(j).getNodeValue(); 67 | break; 68 | } 69 | } 70 | result.put(value, attr); 71 | }); 72 | 73 | return result; 74 | } 75 | 76 | /** 77 | * Extracts text content from {@link Document} 78 | * 79 | * @param document document 80 | * @param path path 81 | * @return text attribute 82 | * @throws XPathExpressionException 83 | */ 84 | public static Optional getTextContent(Document document, String path) throws XPathExpressionException { 85 | return getNodes(document, path).stream() 86 | .findFirst() 87 | .map(Node::getTextContent) 88 | .or(Optional::empty); 89 | } 90 | 91 | private static List getNodes(Document document, String path) throws XPathExpressionException { 92 | var expr = XPathFactory.newInstance().newXPath().compile(path); 93 | var nodes = (NodeList) expr.evaluate(document, XPathConstants.NODESET); 94 | return IntStream.range(0, nodes.getLength()) 95 | .mapToObj(nodes::item) 96 | .toList(); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/app/tozzi/MailParserTest.java: -------------------------------------------------------------------------------- 1 | package app.tozzi; 2 | 3 | import app.tozzi.model.*; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.IOException; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | public class MailParserTest { 11 | 12 | @Test 13 | public void mailTest_1() throws IOException { 14 | 15 | try (var inputStream = getClass().getClassLoader().getResourceAsStream("Test - Simple Mail.eml")) { 16 | var pe = MailParser.getInstance(true).parse(inputStream); 17 | assertNotNull(pe); 18 | assertInstanceOf(Mail.class, pe); 19 | assertEquals(ParsedEntityType.MAIL, pe.getType()); 20 | var mail = (Mail) pe; 21 | assertEquals("EA+dAJ_-oQ-6nyBKVWEFWJyq2Qx_sboF=k9VrediOGf8OO+v7rg@mail.gmail.com", mail.getMessageID()); 22 | assertNotNull(mail.getTo()); 23 | assertEquals(1, mail.getTo().size()); 24 | assertEquals("biagio.tozzi@gmail.com", mail.getTo().get(0).getEmail()); 25 | assertEquals("Biagio Tozzi", mail.getTo().get(0).getName()); 26 | assertNotNull(mail.getFrom()); 27 | assertEquals(1, mail.getFrom().size()); 28 | assertEquals("test@tozzi.app", mail.getFrom().get(0).getEmail()); 29 | assertEquals("Tozzi APP", mail.getFrom().get(0).getName()); 30 | assertNotNull(mail.getCc()); 31 | assertEquals(0, mail.getCc().size()); 32 | assertNotNull(mail.getBcc()); 33 | assertEquals(0, mail.getBcc().size()); 34 | assertNotNull(mail.getBodyHTML()); 35 | assertTrue(mail.getBodyHTML().contains("
Simple Mail!
")); 36 | assertNotNull(mail.getBodyTXT()); 37 | assertTrue(mail.getBodyTXT().contains("Simple Mail!")); 38 | assertNotNull(mail.getSentDate()); 39 | assertNull(mail.getDeliveryStatus()); 40 | assertNotNull(mail.getAttachments()); 41 | assertEquals(1, mail.getAttachments().size()); 42 | assertEquals("640px-Flag_of_Italy.svg.png", mail.getAttachments().get(0).getName()); 43 | assertEquals("ii_m3it0i7x0", mail.getAttachments().get(0).getXAttachmentID()); 44 | assertEquals("", mail.getAttachments().get(0).getContentID()); 45 | assertTrue(mail.getAttachments().get(0).isInline()); 46 | assertNotNull(mail.getAttachments().get(0).getDataSource()); 47 | assertNotNull(mail.getAttachments().get(0).getDataSource().getInputStream()); 48 | assertTrue(mail.getAttachments().get(0).getDataSource().getInputStream().available() > 0); 49 | assertNotNull(mail.getHeaders()); 50 | assertFalse(mail.getHeaders().isEmpty()); 51 | assertFalse(mail.isHasDeliveryStatus()); 52 | assertNull(mail.getReplyToMessageID()); 53 | assertNull(mail.getReplyToHistoryMessagesID()); 54 | } 55 | } 56 | 57 | @Test 58 | public void mailTest_2() throws IOException { 59 | 60 | try (var inputStream = getClass().getClassLoader().getResourceAsStream("Test - Simple Mail.eml")) { 61 | var pe = MailParser.getInstance(false).parse(inputStream); 62 | assertNotNull(pe); 63 | assertInstanceOf(Mail.class, pe); 64 | assertEquals(ParsedEntityType.MAIL, pe.getType()); 65 | var mail = (Mail) pe; 66 | assertEquals("EA+dAJ_-oQ-6nyBKVWEFWJyq2Qx_sboF=k9VrediOGf8OO+v7rg@mail.gmail.com", mail.getMessageID()); 67 | assertNotNull(mail.getTo()); 68 | assertEquals(1, mail.getTo().size()); 69 | assertEquals("biagio.tozzi@gmail.com", mail.getTo().get(0).getEmail()); 70 | assertEquals("Biagio Tozzi", mail.getTo().get(0).getName()); 71 | assertNotNull(mail.getFrom()); 72 | assertEquals(1, mail.getFrom().size()); 73 | assertEquals("test@tozzi.app", mail.getFrom().get(0).getEmail()); 74 | assertEquals("Tozzi APP", mail.getFrom().get(0).getName()); 75 | assertNotNull(mail.getCc()); 76 | assertEquals(0, mail.getCc().size()); 77 | assertNotNull(mail.getBcc()); 78 | assertEquals(0, mail.getBcc().size()); 79 | assertNotNull(mail.getBodyHTML()); 80 | assertTrue(mail.getBodyHTML().contains("
Simple Mail!
")); 81 | assertNotNull(mail.getBodyTXT()); 82 | assertTrue(mail.getBodyTXT().contains("Simple Mail!")); 83 | assertNotNull(mail.getSentDate()); 84 | assertNull(mail.getDeliveryStatus()); 85 | assertNotNull(mail.getAttachments()); 86 | assertEquals(1, mail.getAttachments().size()); 87 | assertEquals("640px-Flag_of_Italy.svg.png", mail.getAttachments().get(0).getName()); 88 | assertEquals("ii_m3it0i7x0", mail.getAttachments().get(0).getXAttachmentID()); 89 | assertEquals("", mail.getAttachments().get(0).getContentID()); 90 | assertTrue(mail.getAttachments().get(0).isInline()); 91 | assertNotNull(mail.getAttachments().get(0).getDataSource()); 92 | assertNotNull(mail.getAttachments().get(0).getDataSource().getInputStream()); 93 | assertTrue(mail.getAttachments().get(0).getDataSource().getInputStream().available() > 0); 94 | assertNull(mail.getHeaders()); 95 | assertFalse(mail.isHasDeliveryStatus()); 96 | assertNull(mail.getReplyToMessageID()); 97 | assertNull(mail.getReplyToHistoryMessagesID()); 98 | } 99 | } 100 | 101 | @Test 102 | public void mailTest_3() throws IOException { 103 | 104 | try (var inputStream = getClass().getClassLoader().getResourceAsStream("Test - Simple Mail (2).eml")) { 105 | var pe = MailParser.getInstance(false).parse(inputStream); 106 | assertNotNull(pe); 107 | assertInstanceOf(Mail.class, pe); 108 | assertEquals(ParsedEntityType.MAIL, pe.getType()); 109 | var mail = (Mail) pe; 110 | assertEquals("EA+dAJ_-Dr55a82Z-wzjQNz=Wku2KADjLWBEOTY2=umsUawTNTQ@mail.gmail.com", mail.getMessageID()); 111 | assertNotNull(mail.getTo()); 112 | assertEquals(1, mail.getTo().size()); 113 | assertEquals("biagio.tozzi@gmail.com", mail.getTo().get(0).getEmail()); 114 | assertEquals("Biagio Tozzi", mail.getTo().get(0).getName()); 115 | assertNotNull(mail.getFrom()); 116 | assertEquals(1, mail.getFrom().size()); 117 | assertEquals("test@tozzi.app", mail.getFrom().get(0).getEmail()); 118 | assertEquals("Tozzi APP", mail.getFrom().get(0).getName()); 119 | assertNotNull(mail.getCc()); 120 | assertEquals(0, mail.getCc().size()); 121 | assertNotNull(mail.getBcc()); 122 | assertEquals(0, mail.getBcc().size()); 123 | assertNotNull(mail.getBodyHTML()); 124 | assertTrue(mail.getBodyHTML().contains("
Test - Simple Mail 2!
")); 125 | assertNotNull(mail.getBodyTXT()); 126 | assertTrue(mail.getBodyTXT().contains("Simple Mail 2!")); 127 | assertNotNull(mail.getSentDate()); 128 | assertNull(mail.getDeliveryStatus()); 129 | assertNotNull(mail.getAttachments()); 130 | assertEquals(1, mail.getAttachments().size()); 131 | assertEquals("640px-Flag_of_Italy.svg.png", mail.getAttachments().get(0).getName()); 132 | assertEquals("f_m3iui2v40", mail.getAttachments().get(0).getXAttachmentID()); 133 | assertEquals("", mail.getAttachments().get(0).getContentID()); 134 | assertFalse(mail.getAttachments().get(0).isInline()); 135 | assertNotNull(mail.getAttachments().get(0).getDataSource()); 136 | assertNotNull(mail.getAttachments().get(0).getDataSource().getInputStream()); 137 | assertTrue(mail.getAttachments().get(0).getDataSource().getInputStream().available() > 0); 138 | assertNull(mail.getHeaders()); 139 | assertFalse(mail.isHasDeliveryStatus()); 140 | assertNull(mail.getReplyToMessageID()); 141 | assertNull(mail.getReplyToHistoryMessagesID()); 142 | } 143 | } 144 | 145 | @Test 146 | public void mailTest_4() throws IOException { 147 | 148 | try (var inputStream = getClass().getClassLoader().getResourceAsStream("Test - Simple Mail (3).eml")) { 149 | var pe = MailParser.getInstance(false).parse(inputStream); 150 | assertNotNull(pe); 151 | assertInstanceOf(Mail.class, pe); 152 | assertEquals(ParsedEntityType.MAIL, pe.getType()); 153 | var mail = (Mail) pe; 154 | assertEquals("EA+wfBhpup681yEmHDfUjNeBUg-sVGumaX76c6iigenOaT_p_aQ@mail.gmail.com", mail.getMessageID()); 155 | assertNotNull(mail.getTo()); 156 | assertEquals(1, mail.getTo().size()); 157 | assertEquals("test@tozzi.app", mail.getTo().get(0).getEmail()); 158 | assertEquals("Tozzi APP", mail.getTo().get(0).getName()); 159 | assertNotNull(mail.getFrom()); 160 | assertEquals(1, mail.getFrom().size()); 161 | assertEquals("biagio.tozzi@gmail.com", mail.getFrom().get(0).getEmail()); 162 | assertEquals("Biagio Tozzi", mail.getFrom().get(0).getName()); 163 | assertNotNull(mail.getCc()); 164 | assertEquals(1, mail.getCc().size()); 165 | assertNull(mail.getCc().get(0).getName()); 166 | assertEquals("test2@tozzi.app", mail.getCc().get(0).getEmail()); 167 | assertNotNull(mail.getBcc()); 168 | assertEquals(0, mail.getBcc().size()); 169 | assertNotNull(mail.getBodyHTML()); 170 | assertTrue(mail.getBodyHTML().contains("Reply!")); 171 | assertNotNull(mail.getBodyTXT()); 172 | assertTrue(mail.getBodyTXT().contains("Reply!")); 173 | assertNotNull(mail.getSentDate()); 174 | assertNull(mail.getDeliveryStatus()); 175 | assertNotNull(mail.getAttachments()); 176 | assertEquals(0, mail.getAttachments().size()); 177 | assertNull(mail.getHeaders()); 178 | assertFalse(mail.isHasDeliveryStatus()); 179 | assertNotNull(mail.getReplyToMessageID()); 180 | assertEquals("EA+dAJ_-Dr55a82Z-wzjQNz=Wku2KADjLWBEOTY2=umsUawTNTQ@mail.gmail.com", mail.getReplyToMessageID()); 181 | assertNotNull(mail.getReplyToHistoryMessagesID()); 182 | assertEquals(1, mail.getReplyToHistoryMessagesID().size()); 183 | assertEquals("EA+dAJ_-Dr55a82Z-wzjQNz=Wku2KADjLWBEOTY2=umsUawTNTQ@mail.gmail.com", mail.getReplyToHistoryMessagesID().get(0)); 184 | } 185 | } 186 | 187 | @Test 188 | public void deliveryStatusTest() throws IOException { 189 | 190 | try (var inputStream = getClass().getClassLoader().getResourceAsStream("Delivery Status Notification (Failure).eml")) { 191 | var pe = MailParser.getInstance(true).parse(inputStream); 192 | assertNotNull(pe); 193 | assertInstanceOf(Mail.class, pe); 194 | assertEquals(ParsedEntityType.MAIL, pe.getType()); 195 | var mail = (Mail) pe; 196 | assertEquals("773764b2.050a0220.544c6.2d4b.GMR@mx.google.com", mail.getMessageID()); 197 | assertNotNull(mail.getFrom()); 198 | assertEquals(1, mail.getFrom().size()); 199 | assertEquals("mailer-daemon@googlemail.com", mail.getFrom().get(0).getEmail()); 200 | assertNotNull(mail.getFrom()); 201 | assertEquals(1, mail.getTo().size()); 202 | assertEquals("biagio.tozzi@gmail.com", mail.getTo().get(0).getEmail()); 203 | assertNotNull(mail.getCc()); 204 | assertEquals(0, mail.getCc().size()); 205 | assertNotNull(mail.getBcc()); 206 | assertEquals(0, mail.getBcc().size()); 207 | assertTrue(mail.isHasDeliveryStatus()); 208 | assertNotNull(mail.getDeliveryStatus()); 209 | assertEquals("5.7.0", mail.getDeliveryStatus().getStatus()); 210 | assertEquals(DeliveryStatus.Action.FAILED, mail.getDeliveryStatus().getAction()); 211 | assertEquals("dns", mail.getDeliveryStatus().getReportingMTA().getType()); 212 | assertEquals("googlemail.com", mail.getDeliveryStatus().getReportingMTA().getAddress()); 213 | assertEquals("dns", mail.getDeliveryStatus().getRemoteMTA().getType()); 214 | assertEquals("mx01.ionos.it. (217.72.192.00, the server for the domain tozzi.app.)", mail.getDeliveryStatus().getRemoteMTA().getAddress()); 215 | assertEquals("rfc822", mail.getDeliveryStatus().getFinalRecipient().getType()); 216 | assertEquals("test@tozzi.app", mail.getDeliveryStatus().getFinalRecipient().getAddress()); 217 | assertEquals(DeliveryStatus.StatusType.PERM_FAILURE, mail.getDeliveryStatus().getStatusType()); 218 | assertNotNull(mail.getDeliveryStatus().getDiagnosticCode()); 219 | assertEquals("smtp", mail.getDeliveryStatus().getDiagnosticCode().getType()); 220 | assertEquals("550-Requested action not taken: mailbox unavailable", mail.getDeliveryStatus().getDiagnosticCode().getDescription()); 221 | assertNotNull(mail.getReplyToMessageID()); 222 | assertEquals("EA+wfBhrSAT7Jc6+8Y_pi_x6SgRzJO5CT6Mc-uKb57n6aff+u9g@mail.gmail.com", mail.getReplyToMessageID()); 223 | assertNotNull(mail.getReplyToHistoryMessagesID()); 224 | assertEquals(1, mail.getReplyToHistoryMessagesID().size()); 225 | assertEquals("EA+wfBhrSAT7Jc6+8Y_pi_x6SgRzJO5CT6Mc-uKb57n6aff+u9g@mail.gmail.com", mail.getReplyToHistoryMessagesID().get(0)); 226 | } 227 | } 228 | 229 | @Test 230 | public void pecReceipt_1() throws IOException { 231 | try (var inputStream = getClass().getClassLoader().getResourceAsStream("accettazione.eml")) { 232 | var pe = MailParser.getInstance(true).parse(inputStream); 233 | assertNotNull(pe); 234 | assertInstanceOf(PECReceipt.class, pe); 235 | assertEquals(ParsedEntityType.PEC_RECEIPT, pe.getType()); 236 | var receipt = (PECReceipt) pe; 237 | assertNotNull(receipt.getPec()); 238 | assertNotNull(receipt.getCertificateData()); 239 | assertEquals("sender@fakepec.it", receipt.getCertificateData().getSender()); 240 | assertEquals("rec@fakepec.it", receipt.getCertificateData().getRecipients().get(0).getAddress()); 241 | assertEquals(CertificateData.PECRecipients.PECRecipientType.CERTIFICATO, receipt.getCertificateData().getRecipients().get(0).getType()); 242 | assertEquals("", receipt.getCertificateData().getMessageID()); 243 | assertEquals("opec210312.20241115182038.288127.606.1.53@fakepec.it", receipt.getCertificateData().getId()); 244 | assertEquals(CertificateData.PECError.NESSUNO, receipt.getCertificateData().getError()); 245 | assertNull(receipt.getCertificateData().getExtendedError()); 246 | assertEquals("sender@fakepec.it", receipt.getCertificateData().getAnswers()); 247 | assertEquals("Test PEC", receipt.getCertificateData().getSubject()); 248 | assertEquals("FAKEPEC PEC S.p.A.", receipt.getCertificateData().getIssuer()); 249 | assertEquals(CertificateData.PostaCertType.ACCETTAZIONE, receipt.getCertificateData().getType()); 250 | assertNotNull(receipt.getPec().getCertificateData()); 251 | assertEquals("sender@fakepec.it", receipt.getPec().getCertificateData().getSender()); 252 | assertEquals("rec@fakepec.it", receipt.getPec().getCertificateData().getRecipients().get(0).getAddress()); 253 | assertEquals(CertificateData.PECRecipients.PECRecipientType.CERTIFICATO, receipt.getPec().getCertificateData().getRecipients().get(0).getType()); 254 | assertEquals("", receipt.getPec().getCertificateData().getMessageID()); 255 | assertEquals("opec210312.20241115182038.288127.606.1.53@fakepec.it", receipt.getPec().getCertificateData().getId()); 256 | assertEquals(CertificateData.PECError.NESSUNO, receipt.getPec().getCertificateData().getError()); 257 | assertNull(receipt.getPec().getCertificateData().getExtendedError()); 258 | assertEquals("sender@fakepec.it", receipt.getPec().getCertificateData().getAnswers()); 259 | assertEquals("Test PEC", receipt.getPec().getCertificateData().getSubject()); 260 | assertEquals("FAKEPEC PEC S.p.A.", receipt.getPec().getCertificateData().getIssuer()); 261 | assertEquals(CertificateData.PostaCertType.ACCETTAZIONE, receipt.getPec().getCertificateData().getType()); 262 | assertNotNull(receipt.getPec().getCertificateData().getDate().getDay()); 263 | assertNotNull(receipt.getPec().getCertificateData().getDate().getHour()); 264 | assertNotNull(receipt.getPec().getCertificateData().getDate().getZone()); 265 | assertNull(receipt.getPec().getOriginalMessage()); 266 | assertNotNull(receipt.getPec().getDatiCert()); 267 | assertNull(receipt.getPec().getPostaCert()); 268 | assertNotNull(receipt.getPec().getEnvelope()); 269 | assertEquals("ACCETTAZIONE: Test PEC", receipt.getPec().getEnvelope().getSubject()); 270 | assertEquals("posta-certificata@fakepec.it", receipt.getPec().getEnvelope().getFrom().get(0).getEmail()); 271 | assertEquals("opec210312.20241115182038.288127.606.1.771.53@fakepec.it", receipt.getPec().getEnvelope().getMessageID()); 272 | assertFalse(receipt.getPec().getEnvelope().isHasDeliveryStatus()); 273 | assertEquals("sender@fakepec.it", receipt.getPec().getEnvelope().getTo().get(0).getEmail()); 274 | } 275 | } 276 | 277 | @Test 278 | public void pecReceipt_2() throws IOException { 279 | try (var inputStream = getClass().getClassLoader().getResourceAsStream("consegna.eml")) { 280 | var pe = MailParser.getInstance(true).parse(inputStream); 281 | assertNotNull(pe); 282 | assertInstanceOf(PECReceipt.class, pe); 283 | assertEquals(ParsedEntityType.PEC_RECEIPT, pe.getType()); 284 | var receipt = (PECReceipt) pe; 285 | assertNotNull(receipt.getPec()); 286 | assertNotNull(receipt.getCertificateData()); 287 | assertEquals("sender@fakepec.it", receipt.getCertificateData().getSender()); 288 | assertEquals("rec@pec.it", receipt.getCertificateData().getRecipients().get(0).getAddress()); 289 | assertEquals(CertificateData.PECRecipients.PECRecipientType.CERTIFICATO, receipt.getCertificateData().getRecipients().get(0).getType()); 290 | assertEquals("", receipt.getCertificateData().getMessageID()); 291 | assertEquals("opec210312.20241115182038.288127.606.1.53@fakepec.it", receipt.getCertificateData().getId()); 292 | assertEquals(CertificateData.PECError.NO_DEST, receipt.getCertificateData().getError()); 293 | assertNotNull(receipt.getCertificateData().getExtendedError()); 294 | assertEquals("5.1.1 - FAKE Pec S.p.A. - indirizzo non valido", receipt.getCertificateData().getExtendedError()); 295 | assertEquals("sender@fakepec.it", receipt.getCertificateData().getAnswers()); 296 | assertEquals("Test PEC", receipt.getCertificateData().getSubject()); 297 | assertEquals("FAKE PEC S.p.A.", receipt.getCertificateData().getIssuer()); 298 | assertEquals(CertificateData.PostaCertType.ERRORE_CONSEGNA, receipt.getCertificateData().getType()); 299 | assertNotNull(receipt.getPec().getCertificateData()); 300 | assertEquals("sender@fakepec.it", receipt.getPec().getCertificateData().getSender()); 301 | assertEquals("rec@pec.it", receipt.getPec().getCertificateData().getRecipients().get(0).getAddress()); 302 | assertEquals(CertificateData.PECRecipients.PECRecipientType.CERTIFICATO, receipt.getPec().getCertificateData().getRecipients().get(0).getType()); 303 | assertEquals("", receipt.getPec().getCertificateData().getMessageID()); 304 | assertEquals("opec210312.20241115182038.288127.606.1.53@fakepec.it", receipt.getPec().getCertificateData().getId()); 305 | assertEquals(CertificateData.PECError.NO_DEST, receipt.getPec().getCertificateData().getError()); 306 | assertNotNull(receipt.getPec().getCertificateData().getExtendedError()); 307 | assertEquals("sender@fakepec.it", receipt.getPec().getCertificateData().getAnswers()); 308 | assertEquals("Test PEC", receipt.getPec().getCertificateData().getSubject()); 309 | assertEquals("FAKE PEC S.p.A.", receipt.getPec().getCertificateData().getIssuer()); 310 | assertEquals(CertificateData.PostaCertType.ERRORE_CONSEGNA, receipt.getPec().getCertificateData().getType()); 311 | assertNotNull(receipt.getPec().getCertificateData().getDate().getDay()); 312 | assertNotNull(receipt.getPec().getCertificateData().getDate().getHour()); 313 | assertNotNull(receipt.getPec().getCertificateData().getDate().getZone()); 314 | assertNull(receipt.getPec().getOriginalMessage()); 315 | assertNotNull(receipt.getPec().getDatiCert()); 316 | assertNull(receipt.getPec().getPostaCert()); 317 | assertNotNull(receipt.getPec().getEnvelope()); 318 | assertEquals("AVVISO DI MANCATA CONSEGNA: Test PEC", receipt.getPec().getEnvelope().getSubject()); 319 | assertEquals("posta-certificata@fakepec.it", receipt.getPec().getEnvelope().getFrom().get(0).getEmail()); 320 | assertEquals("opec210312.20241115182103.186549.961.1.530.42@fakepec.it", receipt.getPec().getEnvelope().getMessageID()); 321 | assertFalse(receipt.getPec().getEnvelope().isHasDeliveryStatus()); 322 | assertEquals("sender@fakepec.it", receipt.getPec().getEnvelope().getTo().get(0).getEmail()); 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/test/java/app/tozzi/core/DeliveryStatusHandlerTest.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.core; 2 | 3 | import app.tozzi.model.DeliveryStatus; 4 | import app.tozzi.model.exception.MailParserException; 5 | import jakarta.mail.MessagingException; 6 | import jakarta.mail.internet.MimePart; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | public class DeliveryStatusHandlerTest { 16 | 17 | @Test 18 | void testLoadDeliveryStatus_withValidData() throws Exception { 19 | var sampleContent = "Action: failed\n" + 20 | "Status: 5.1.1\n" + 21 | "Diagnostic-Code: smtp; 550 5.1.1 Recipient address rejected\n" + 22 | "Remote-MTA: dns; mail.example.com\n" + 23 | "Reporting-MTA: dns; mailserver.example.com\n" + 24 | "Received-From-MTA: dns; smtp.example.com\n" + 25 | "Final-Recipient: rfc822; user@example.com\n"; 26 | 27 | var part = Mockito.mock(MimePart.class); 28 | try (var inputStream = new ByteArrayInputStream(sampleContent.getBytes())) { 29 | Mockito.when(part.getInputStream()).thenReturn(inputStream); 30 | var deliveryStatus = DeliveryStatusHandler.loadDeliveryStatus(part); 31 | assertEquals(DeliveryStatus.Action.FAILED, deliveryStatus.getAction()); 32 | assertEquals("5.1.1", deliveryStatus.getStatus()); 33 | assertEquals(DeliveryStatus.StatusType.PERM_FAILURE, deliveryStatus.getStatusType()); 34 | assertEquals("smtp", deliveryStatus.getDiagnosticCode().getType()); 35 | assertEquals("550 5.1.1 Recipient address rejected", deliveryStatus.getDiagnosticCode().getDescription()); 36 | assertEquals("dns", deliveryStatus.getRemoteMTA().getType()); 37 | assertEquals("mail.example.com", deliveryStatus.getRemoteMTA().getAddress()); 38 | assertEquals("dns", deliveryStatus.getReportingMTA().getType()); 39 | assertEquals("mailserver.example.com", deliveryStatus.getReportingMTA().getAddress()); 40 | assertEquals("dns", deliveryStatus.getReceivedFromMTA().getType()); 41 | assertEquals("smtp.example.com", deliveryStatus.getReceivedFromMTA().getName()); 42 | assertEquals("rfc822", deliveryStatus.getFinalRecipient().getType()); 43 | assertEquals("user@example.com", deliveryStatus.getFinalRecipient().getAddress()); 44 | } 45 | } 46 | 47 | @Test 48 | void testLoadDeliveryStatus_withMissingFields() throws Exception { 49 | var sampleContent = "Action: delayed\n" + 50 | "Status: 4.4.1\n" + 51 | "Diagnostic-Code: smtp; 421 4.4.1 Timeout\n" + 52 | "Final-Recipient: rfc822; admin@example.com\n"; 53 | 54 | var part = Mockito.mock(MimePart.class); 55 | try (var inputStream = new ByteArrayInputStream(sampleContent.getBytes())) { 56 | Mockito.when(part.getInputStream()).thenReturn(inputStream); 57 | var deliveryStatus = DeliveryStatusHandler.loadDeliveryStatus(part); 58 | assertEquals(DeliveryStatus.Action.DELAYED, deliveryStatus.getAction()); 59 | assertEquals("4.4.1", deliveryStatus.getStatus()); 60 | assertEquals(DeliveryStatus.StatusType.TEMP_FAILURE, deliveryStatus.getStatusType()); 61 | assertEquals("smtp", deliveryStatus.getDiagnosticCode().getType()); 62 | assertEquals("421 4.4.1 Timeout", deliveryStatus.getDiagnosticCode().getDescription()); 63 | assertEquals("rfc822", deliveryStatus.getFinalRecipient().getType()); 64 | assertEquals("admin@example.com", deliveryStatus.getFinalRecipient().getAddress()); 65 | assertNull(deliveryStatus.getRemoteMTA()); 66 | assertNull(deliveryStatus.getReportingMTA()); 67 | assertNull(deliveryStatus.getReceivedFromMTA()); 68 | } 69 | } 70 | 71 | @Test 72 | void testLoadDeliveryStatus_withInvalidData() throws Exception { 73 | var sampleContent = "InvalidField: some_value\n"; 74 | var part = Mockito.mock(MimePart.class); 75 | try (var inputStream = new ByteArrayInputStream(sampleContent.getBytes())) { 76 | Mockito.when(part.getInputStream()).thenReturn(inputStream); 77 | var deliveryStatus = DeliveryStatusHandler.loadDeliveryStatus(part); 78 | assertNotNull(deliveryStatus); 79 | assertNull(deliveryStatus.getAction()); 80 | assertNull(deliveryStatus.getStatus()); 81 | assertNull(deliveryStatus.getDiagnosticCode()); 82 | assertNull(deliveryStatus.getRemoteMTA()); 83 | assertNull(deliveryStatus.getReportingMTA()); 84 | assertNull(deliveryStatus.getReceivedFromMTA()); 85 | assertNull(deliveryStatus.getFinalRecipient()); 86 | } 87 | } 88 | 89 | @Test 90 | void testLoadDeliveryStatus_withEmptyInput() throws Exception { 91 | String sampleContent = ""; 92 | var part = Mockito.mock(MimePart.class); 93 | try (var inputStream = new ByteArrayInputStream(sampleContent.getBytes())) { 94 | Mockito.when(part.getInputStream()).thenReturn(inputStream); 95 | var deliveryStatus = DeliveryStatusHandler.loadDeliveryStatus(part); 96 | assertNotNull(deliveryStatus); 97 | assertNull(deliveryStatus.getAction()); 98 | assertNull(deliveryStatus.getStatus()); 99 | assertNull(deliveryStatus.getDiagnosticCode()); 100 | assertNull(deliveryStatus.getRemoteMTA()); 101 | assertNull(deliveryStatus.getReportingMTA()); 102 | assertNull(deliveryStatus.getReceivedFromMTA()); 103 | assertNull(deliveryStatus.getFinalRecipient()); 104 | } 105 | } 106 | 107 | @Test 108 | void testLoadDeliveryStatus_withException() throws MessagingException, IOException { 109 | var part = Mockito.mock(MimePart.class); 110 | Mockito.when(part.getInputStream()).thenThrow(new RuntimeException("Simulated error")); 111 | assertThrows(MailParserException.class, () -> DeliveryStatusHandler.loadDeliveryStatus(part)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/app/tozzi/util/IOUtilsTest.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import jakarta.activation.DataHandler; 4 | import jakarta.activation.DataSource; 5 | import jakarta.mail.Part; 6 | import jakarta.mail.internet.MimePart; 7 | import jakarta.mail.util.ByteArrayDataSource; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.MockedStatic; 10 | 11 | import java.io.ByteArrayInputStream; 12 | import java.io.ByteArrayOutputStream; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertNotNull; 16 | import static org.mockito.Mockito.*; 17 | 18 | public class IOUtilsTest { 19 | 20 | private static final String SAMPLE_CONTENT = "Sample Content"; 21 | private static final String SAMPLE_FILE_NAME = "test.txt"; 22 | 23 | @Test 24 | void testCreateDataSourceFromInputStream() throws Exception { 25 | try (var inputStream = new ByteArrayInputStream(SAMPLE_CONTENT.getBytes())) { 26 | var dataSource = IOUtils.createDataSource(inputStream, SAMPLE_FILE_NAME); 27 | assertNotNull(dataSource); 28 | assertEquals(SAMPLE_FILE_NAME, dataSource.getName()); 29 | assertEquals(MimeTypesUtil.CONTENT_TYPE_TEXT_PLAIN, dataSource.getContentType()); 30 | assertEquals(SAMPLE_CONTENT, new String(dataSource.getInputStream().readAllBytes())); 31 | } 32 | } 33 | 34 | @Test 35 | void testCreateDataSourceFromMimePart() throws Exception { 36 | var part = mock(MimePart.class); 37 | 38 | try (var partInputStream = new ByteArrayInputStream(SAMPLE_CONTENT.getBytes())) { 39 | try (MockedStatic utilsMock = mockStatic(MimeMessageUtils.class)) { 40 | var ds = new ByteArrayDataSource(partInputStream, MimeTypesUtil.CONTENT_TYPE_TEXT_PLAIN); 41 | ds.setName(SAMPLE_FILE_NAME); 42 | when(MimeMessageUtils.getDataHandler(part)).thenReturn(new DataHandler(ds)); 43 | when(MimeMessageUtils.getFileName(part)).thenReturn(SAMPLE_FILE_NAME); 44 | var dataSource = IOUtils.createDataSource(part, SAMPLE_FILE_NAME); 45 | assertNotNull(dataSource); 46 | assertEquals(SAMPLE_FILE_NAME, dataSource.getName()); 47 | assertEquals(MimeTypesUtil.CONTENT_TYPE_TEXT_PLAIN, dataSource.getContentType()); 48 | assertEquals(SAMPLE_CONTENT, new String(dataSource.getInputStream().readAllBytes())); 49 | } 50 | } 51 | } 52 | 53 | @Test 54 | void testFastCopy() throws Exception { 55 | try (var inputStream = new ByteArrayInputStream(SAMPLE_CONTENT.getBytes()); var outputStream = new ByteArrayOutputStream()) { 56 | IOUtils.fastCopy(inputStream, outputStream); 57 | assertEquals(SAMPLE_CONTENT, outputStream.toString()); 58 | } 59 | } 60 | 61 | @Test 62 | void testGetFileMimeType() { 63 | try (MockedStatic utilsMock = mockStatic(MimeTypesUtil.class)) { 64 | when(MimeTypesUtil.guessMimeType("txt")).thenReturn(MimeTypesUtil.CONTENT_TYPE_TEXT_PLAIN); 65 | var mimeType = IOUtils.getFileMimeType(SAMPLE_FILE_NAME); 66 | assertEquals(MimeTypesUtil.CONTENT_TYPE_TEXT_PLAIN, mimeType); 67 | } 68 | } 69 | 70 | @Test 71 | void testGetName() { 72 | try (MockedStatic utilsMock = mockStatic(MimeMessageUtils.class)) { 73 | var part = mock(Part.class); 74 | when(MimeMessageUtils.getFileName(part)).thenReturn(SAMPLE_FILE_NAME); 75 | when(MimeMessageUtils.decodeText(anyString())).thenReturn(SAMPLE_FILE_NAME); 76 | var name = IOUtils.getName(part); 77 | assertEquals(SAMPLE_FILE_NAME, name); 78 | } 79 | } 80 | 81 | @Test 82 | void testGetContent() throws Exception { 83 | try (var inputStream = new ByteArrayInputStream(SAMPLE_CONTENT.getBytes())) { 84 | var content = IOUtils.getContent(inputStream); 85 | assertNotNull(content); 86 | assertEquals(SAMPLE_CONTENT, new String(content)); 87 | } 88 | } 89 | 90 | @Test 91 | void testCreateDataSourceWithFallbackName() throws Exception { 92 | try (var inputStream = new ByteArrayInputStream(SAMPLE_CONTENT.getBytes())) { 93 | DataSource dataSource = IOUtils.createDataSource(inputStream, null); 94 | assertNotNull(dataSource); 95 | assertEquals("unknown_name", dataSource.getName()); 96 | assertEquals(MimeTypesUtil.CONTENT_TYPE_OCTETSTREAM, dataSource.getContentType()); 97 | assertEquals(SAMPLE_CONTENT, new String(dataSource.getInputStream().readAllBytes())); 98 | } 99 | } 100 | 101 | @Test 102 | void testCreateDataSource() throws Exception { 103 | try (var inputStream = new ByteArrayInputStream(SAMPLE_CONTENT.getBytes())) { 104 | DataSource dataSource = IOUtils.createDataSource(inputStream, SAMPLE_FILE_NAME); 105 | assertNotNull(dataSource); 106 | assertEquals(SAMPLE_FILE_NAME, dataSource.getName()); 107 | assertEquals(MimeTypesUtil.CONTENT_TYPE_TEXT_PLAIN, dataSource.getContentType()); 108 | assertEquals(SAMPLE_CONTENT, new String(dataSource.getInputStream().readAllBytes())); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/app/tozzi/util/MimeMessageUtilsTest.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import app.tozzi.model.exception.MailParserException; 4 | import jakarta.activation.DataHandler; 5 | import jakarta.mail.Message; 6 | import jakarta.mail.MessagingException; 7 | import jakarta.mail.Part; 8 | import jakarta.mail.internet.InternetAddress; 9 | import jakarta.mail.internet.MimeMessage; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.util.Date; 13 | 14 | import static org.junit.jupiter.api.Assertions.*; 15 | import static org.mockito.Mockito.*; 16 | 17 | public class MimeMessageUtilsTest { 18 | 19 | @Test 20 | void testGetMessageID_withValidID() throws Exception { 21 | var mimeMessage = mock(MimeMessage.class); 22 | when(mimeMessage.getMessageID()).thenReturn("<12345@example.com>"); 23 | var messageID = MimeMessageUtils.getMessageID(mimeMessage); 24 | assertEquals("12345@example.com", messageID); 25 | verify(mimeMessage, times(1)).getMessageID(); 26 | } 27 | 28 | @Test 29 | void testGetMessageID_throwsException() throws Exception { 30 | var mimeMessage = mock(MimeMessage.class); 31 | when(mimeMessage.getMessageID()).thenThrow(new MessagingException("Test Exception")); 32 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getMessageID(mimeMessage)); 33 | } 34 | 35 | @Test 36 | void testGetSubject() throws Exception { 37 | var mimeMessage = mock(MimeMessage.class); 38 | when(mimeMessage.getSubject()).thenReturn("Test Subject"); 39 | var subject = MimeMessageUtils.getSubject(mimeMessage); 40 | assertEquals("Test Subject", subject); 41 | verify(mimeMessage, times(1)).getSubject(); 42 | } 43 | 44 | @Test 45 | void testGetSubject_throwsException() throws Exception { 46 | var mimeMessage = mock(MimeMessage.class); 47 | when(mimeMessage.getSubject()).thenThrow(new MessagingException("Test Exception")); 48 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getSubject(mimeMessage)); 49 | } 50 | 51 | @Test 52 | void testGetTo() throws Exception { 53 | var mimeMessage = mock(MimeMessage.class); 54 | var recipients = new InternetAddress[]{new InternetAddress("to@example.com")}; 55 | when(mimeMessage.getRecipients(Message.RecipientType.TO)).thenReturn(recipients); 56 | var toList = MimeMessageUtils.getTo(mimeMessage); 57 | assertEquals(1, toList.size()); 58 | assertEquals("to@example.com", ((InternetAddress) toList.get(0)).getAddress()); 59 | verify(mimeMessage, times(1)).getRecipients(Message.RecipientType.TO); 60 | } 61 | 62 | @Test 63 | void testGetHeader() throws Exception { 64 | var mimeMessage = mock(MimeMessage.class); 65 | when(mimeMessage.getHeader("X-Test-Header", ",")).thenReturn("HeaderValue"); 66 | var header = MimeMessageUtils.getHeader(mimeMessage, "X-Test-Header"); 67 | assertEquals("HeaderValue", header); 68 | verify(mimeMessage, times(1)).getHeader("X-Test-Header", ","); 69 | } 70 | 71 | @Test 72 | void testGetHeader_throwsException() throws Exception { 73 | var mimeMessage = mock(MimeMessage.class); 74 | when(mimeMessage.getHeader("X-Test-Header", ",")).thenThrow(new MessagingException("Test Exception")); 75 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getHeader(mimeMessage, "X-Test-Header")); 76 | } 77 | 78 | @Test 79 | void testDecodeText() { 80 | var encodedText = "=?UTF-8?B?VGVzdCBUZXh0?="; 81 | var decodedText = "Test Text"; 82 | var result = MimeMessageUtils.decodeText(encodedText); 83 | assertEquals(decodedText, result); 84 | } 85 | 86 | @Test 87 | void testGetReceivedDate() throws Exception { 88 | var mimeMessage = mock(MimeMessage.class); 89 | var receivedDate = new Date(); 90 | when(mimeMessage.getReceivedDate()).thenReturn(receivedDate); 91 | var result = MimeMessageUtils.getReceivedDate(mimeMessage); 92 | assertEquals(receivedDate, result); 93 | verify(mimeMessage, times(1)).getReceivedDate(); 94 | } 95 | 96 | @Test 97 | void testGetReceivedDate_throwsException() throws Exception { 98 | var mimeMessage = mock(MimeMessage.class); 99 | when(mimeMessage.getReceivedDate()).thenThrow(new MessagingException("Test Exception")); 100 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getReceivedDate(mimeMessage)); 101 | } 102 | 103 | @Test 104 | void testGetSentDate() throws Exception { 105 | var mimeMessage = mock(MimeMessage.class); 106 | var sentDate = new Date(); 107 | when(mimeMessage.getSentDate()).thenReturn(sentDate); 108 | var result = MimeMessageUtils.getSentDate(mimeMessage); 109 | assertEquals(sentDate, result); 110 | verify(mimeMessage, times(1)).getSentDate(); 111 | } 112 | 113 | @Test 114 | void testGetSentDate_throwsException() throws Exception { 115 | var mimeMessage = mock(MimeMessage.class); 116 | when(mimeMessage.getSentDate()).thenThrow(new MessagingException("Test Exception")); 117 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getSentDate(mimeMessage)); 118 | } 119 | 120 | @Test 121 | void testGetBCC() throws Exception { 122 | var mimeMessage = mock(MimeMessage.class); 123 | var bccRecipients = new InternetAddress[]{new InternetAddress("bcc@example.com")}; 124 | when(mimeMessage.getRecipients(Message.RecipientType.BCC)).thenReturn(bccRecipients); 125 | var bccList = MimeMessageUtils.getBCC(mimeMessage); 126 | assertEquals(1, bccList.size()); 127 | assertEquals("bcc@example.com", ((InternetAddress) bccList.get(0)).getAddress()); 128 | verify(mimeMessage, times(1)).getRecipients(Message.RecipientType.BCC); 129 | } 130 | 131 | @Test 132 | void testGetBCC_empty() throws Exception { 133 | var mimeMessage = mock(MimeMessage.class); 134 | when(mimeMessage.getRecipients(Message.RecipientType.BCC)).thenReturn(null); 135 | var bccList = MimeMessageUtils.getBCC(mimeMessage); 136 | assertTrue(bccList.isEmpty()); 137 | } 138 | 139 | @Test 140 | void testGetCC() throws Exception { 141 | var mimeMessage = mock(MimeMessage.class); 142 | var ccRecipients = new InternetAddress[]{new InternetAddress("cc@example.com")}; 143 | when(mimeMessage.getRecipients(Message.RecipientType.CC)).thenReturn(ccRecipients); 144 | var ccList = MimeMessageUtils.getCC(mimeMessage); 145 | assertEquals(1, ccList.size()); 146 | assertEquals("cc@example.com", ((InternetAddress) ccList.get(0)).getAddress()); 147 | verify(mimeMessage, times(1)).getRecipients(Message.RecipientType.CC); 148 | } 149 | 150 | @Test 151 | void testGetCC_empty() throws Exception { 152 | var mimeMessage = mock(MimeMessage.class); 153 | when(mimeMessage.getRecipients(Message.RecipientType.CC)).thenReturn(null); 154 | var ccList = MimeMessageUtils.getCC(mimeMessage); 155 | assertTrue(ccList.isEmpty()); 156 | } 157 | 158 | @Test 159 | void testGetContent() throws Exception { 160 | var part = mock(Part.class); 161 | var content = "Sample content"; 162 | when(part.getContent()).thenReturn(content); 163 | var result = MimeMessageUtils.getContent(part); 164 | assertEquals(content, result); 165 | verify(part, times(1)).getContent(); 166 | } 167 | 168 | @Test 169 | void testGetContent_throwsException() throws Exception { 170 | var part = mock(Part.class); 171 | when(part.getContent()).thenThrow(new MessagingException("Test Exception")); 172 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getContent(part)); 173 | } 174 | 175 | @Test 176 | void testGetFileName() throws Exception { 177 | var part = mock(Part.class); 178 | var fileName = "test.txt"; 179 | when(part.getFileName()).thenReturn(fileName); 180 | var result = MimeMessageUtils.getFileName(part); 181 | assertEquals(fileName, result); 182 | verify(part, times(1)).getFileName(); 183 | } 184 | 185 | @Test 186 | void testGetFileName_throwsException() throws Exception { 187 | var part = mock(Part.class); 188 | when(part.getFileName()).thenThrow(new MessagingException("Test Exception")); 189 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getFileName(part)); 190 | } 191 | 192 | @Test 193 | void testGetDataHandler() throws Exception { 194 | var part = mock(Part.class); 195 | var dataHandler = mock(DataHandler.class); 196 | when(part.getDataHandler()).thenReturn(dataHandler); 197 | var result = MimeMessageUtils.getDataHandler(part); 198 | assertEquals(dataHandler, result); 199 | verify(part, times(1)).getDataHandler(); 200 | } 201 | 202 | @Test 203 | void testGetDataHandler_throwsException() throws Exception { 204 | var part = mock(Part.class); 205 | when(part.getDataHandler()).thenThrow(new MessagingException("Test Exception")); 206 | assertThrows(MailParserException.class, () -> MimeMessageUtils.getDataHandler(part)); 207 | } 208 | 209 | 210 | } 211 | -------------------------------------------------------------------------------- /src/test/java/app/tozzi/util/MimeTypesUtilTest.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | 10 | public class MimeTypesUtilTest { 11 | 12 | @Test 13 | void testGuessMimeType_withValidExtension() { 14 | assertEquals("video/x-ms-wmv", MimeTypesUtil.guessMimeType("wmv")); 15 | assertEquals("video/x-msvideo", MimeTypesUtil.guessMimeType("avi")); 16 | assertEquals("application/andrew-inset", MimeTypesUtil.guessMimeType("ez")); 17 | assertEquals("application/octet-stream", MimeTypesUtil.guessMimeType("unknown")); // Estensione sconosciuta 18 | } 19 | 20 | @Test 21 | void testGuessMimeType_withMultipleMatches() { 22 | assertEquals("audio/midi", MimeTypesUtil.guessMimeType("midi")); 23 | } 24 | 25 | @Test 26 | void testGuessMimeType_withNullExtension() { 27 | assertNull(MimeTypesUtil.guessMimeType(null)); 28 | } 29 | 30 | @Test 31 | void testGuessExtension_withValidMimeType() { 32 | assertEquals("wm", MimeTypesUtil.guessExtension("video/x-ms-wmv")); 33 | assertEquals("avi", MimeTypesUtil.guessExtension("video/x-msvideo")); 34 | assertEquals("ez", MimeTypesUtil.guessExtension("application/andrew-inset")); 35 | } 36 | 37 | @Test 38 | void testGuessExtension_withUnknownMimeType() { 39 | assertNull(MimeTypesUtil.guessExtension("application/unknown")); 40 | } 41 | 42 | @Test 43 | void testExcludeMimeTypes_withMatchingPattern() { 44 | var mimeTypes = List.of("video/x-ms-wmv", "video/x-msvideo", "application/andrew-inset"); 45 | assertEquals("video/x-ms-wmv", MimeTypesUtil.excludeMimeTypes(List.of("/vnd."), mimeTypes)); 46 | } 47 | 48 | @Test 49 | void testExcludeMimeTypes_withMultiplePatterns() { 50 | var mimeTypes = List.of("video/x-ms-wmv", "video/x-msvideo", "application/andrew-inset"); 51 | assertEquals("application/andrew-inset", MimeTypesUtil.excludeMimeTypes(List.of("/x-"), mimeTypes)); 52 | } 53 | 54 | @Test 55 | void testExcludeMimeTypes_withNoExclusion() { 56 | var mimeTypes = List.of("video/x-ms-wmv", "video/x-msvideo", "application/andrew-inset"); 57 | assertEquals("video/x-ms-wmv", MimeTypesUtil.excludeMimeTypes(List.of(), mimeTypes)); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/test/java/app/tozzi/util/UUEncodingUtilsTest.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | public class UUEncodingUtilsTest { 10 | 11 | private static final String UUENCODED_CONTENT = """ 12 | begin 644 dcode_uuencode 13 | +965E965E965E964` 14 | ` 15 | end 16 | """; 17 | 18 | @Test 19 | public void containsEncodedAttachments() { 20 | assertTrue(UUEncodingUtils.containsEncodedAttachments(UUENCODED_CONTENT)); 21 | } 22 | 23 | @Test 24 | public void getNextBeginIndex() { 25 | var index = UUEncodingUtils.getNextBeginIndex(UUENCODED_CONTENT); 26 | assertEquals(0, index); 27 | } 28 | 29 | @Test 30 | public void decodeAttachments() throws IOException { 31 | var content = UUEncodingUtils.decodeAttachments(UUENCODED_CONTENT); 32 | assertNotNull(content); 33 | assertEquals(1, content.size()); 34 | assertNotNull(content.get(0).getDataSource()); 35 | assertNotNull(content.get(0).getDataSource().getInputStream()); 36 | assertTrue(content.get(0).getDataSource().getInputStream().available() > 0); 37 | assertEquals("dcode_uuencode", content.get(0).getName()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/app/tozzi/util/XMLUtilsTest.java: -------------------------------------------------------------------------------- 1 | package app.tozzi.util; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.w3c.dom.Document; 6 | 7 | import javax.xml.parsers.DocumentBuilderFactory; 8 | import javax.xml.xpath.XPathExpressionException; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | public class XMLUtilsTest { 13 | 14 | private Document document; 15 | 16 | @BeforeEach 17 | void setUp() throws Exception { 18 | var factory = DocumentBuilderFactory.newInstance(); 19 | var builder = factory.newDocumentBuilder(); 20 | document = builder.newDocument(); 21 | var root = document.createElement("root"); 22 | document.appendChild(root); 23 | var child1 = document.createElement("child"); 24 | child1.setAttribute("id", "1"); 25 | child1.setTextContent("First Child"); 26 | root.appendChild(child1); 27 | var child2 = document.createElement("child"); 28 | child2.setAttribute("id", "2"); 29 | child2.setTextContent("Second Child"); 30 | root.appendChild(child2); 31 | var child3 = document.createElement("child"); 32 | child3.setTextContent("Third Child"); 33 | root.appendChild(child3); 34 | } 35 | 36 | @Test 37 | void testGetAttribute() throws XPathExpressionException { 38 | var attribute = XMLUtils.getAttribute(document, "/root/child", "id"); 39 | assertTrue(attribute.isPresent()); 40 | assertEquals("1", attribute.get()); 41 | } 42 | 43 | @Test 44 | void testGetAttributeNotFound() throws XPathExpressionException { 45 | var attribute = XMLUtils.getAttribute(document, "/root/child", "nonexistent"); 46 | assertTrue(attribute.isEmpty()); 47 | } 48 | 49 | @Test 50 | void testGetTextAndAttribute() throws XPathExpressionException { 51 | var result = XMLUtils.getTextAndAttribute(document, "/root/child", "id"); 52 | assertEquals(3, result.size()); 53 | assertEquals("1", result.get("First Child")); 54 | assertEquals("2", result.get("Second Child")); 55 | assertNull(result.get("Third Child")); 56 | } 57 | 58 | @Test 59 | void testGetTextContent() throws XPathExpressionException { 60 | var textContent = XMLUtils.getTextContent(document, "/root/child"); 61 | assertTrue(textContent.isPresent()); 62 | assertEquals("First Child", textContent.get()); 63 | } 64 | 65 | @Test 66 | void testGetTextContentNotFound() throws XPathExpressionException { 67 | var textContent = XMLUtils.getTextContent(document, "/root/nonexistent"); 68 | assertTrue(textContent.isEmpty()); 69 | } 70 | 71 | @Test 72 | void testGetNodesEmpty() throws XPathExpressionException { 73 | var nodes = XMLUtils.getTextContent(document, "/root/nonexistent"); 74 | assertTrue(nodes.isEmpty()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/resources/Delivery Status Notification (Failure).eml: -------------------------------------------------------------------------------- 1 | Delivered-To: biagio.tozzi@gmail.com 2 | Return-Path: <> 3 | Content-Type: multipart/report; boundary="0000000000004498f20626f4fc34"; report-type=delivery-status 4 | To: biagio.tozzi@gmail.com 5 | Return-Path: <> 6 | Auto-Submitted: auto-replied 7 | Message-ID: <773764b2.050a0220.544c6.2d4b.GMR@mx.google.com> 8 | Date: Fri, 15 Nov 2024 07:11:46 -0800 (PST) 9 | From: Mail Delivery Subsystem 10 | Subject: Delivery Status Notification (Failure) 11 | References: 12 | In-Reply-To: 13 | X-Failed-Recipients: test@tozzi.app 14 | 15 | --0000000000004498f20626f4fc34 16 | Content-Type: multipart/related; boundary="00000000000044b4a00626f4fcab" 17 | 18 | --00000000000044b4a00626f4fcab 19 | Content-Type: multipart/alternative; boundary="00000000000044b4ab0626f4fcac" 20 | 21 | --00000000000044b4ab0626f4fcac 22 | Content-Type: text/plain; charset="UTF-8" 23 | 24 | 25 | ** Message blocked ** 26 | 27 | Your message to test@tozzi.app has been blocked. See technical details below for more information. 28 | 29 | Learn more here: https://postmaster.1und1.de/en/case?c=r1601&i=ip&v=209.85.167.49&r=1N7PDv-1tqRg71qLV-014hm4 30 | (Warning: This link will take you to a third-party site) 31 | 32 | The response from the remote server was: 33 | 550 Requested action not taken: mailbox unavailable For explanation visit https://postmaster.1und1.de/en/case?c=r1601&i=ip&v=209.85.167.49&r=1N7PDv-1tqRg71qLV-014hm4 34 | 35 | --00000000000044b4ab0626f4fcac 36 | Content-Type: text/html; charset="UTF-8" 37 | 38 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 71 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | --00000000000044b4ab0626f4fcac-- 84 | --00000000000044b4a00626f4fcab 85 | Content-Type: image/png; name="icon.png" 86 | Content-Disposition: attachment; filename="icon.png" 87 | Content-Transfer-Encoding: base64 88 | Content-ID: 89 | 90 | iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAAAXNSR0IArs4c6QAAAAlwSFlzAAAW 91 | JQAAFiUBSVIk8AAAActpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6 92 | eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYg 93 | eG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4K 94 | ICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1w 95 | PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJo 96 | dHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+ 97 | QWRvYmUgSW1hZ2VSZWFkeTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRh 98 | dGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9y 99 | ZGY6UkRGPgo8L3g6eG1wbWV0YT4KKS7NPQAAEAVJREFUeAHtnV9sHEcdx+fufH/9344dJ7FjN6SU 100 | FKSmrdqkqkRdoTapKtHjqfACQYgHJCRSAYKCQEHwwBvhBfEChCKkIniwkJBKC9StlOIkNE3IHydp 101 | kzpx7Tixnfi/73znO37f823Zbn23u7N7uzcz+5Os272bv7/5eGZ+Mzu/ZSyQQAOBBgIN+KWBkF8Z 102 | 10u+jzyV3hsOs8FQiO0tFAoDoVDoiWplKxaLb4TD4bFikZ0pFNjwqdeGzlQLL/tvSgL06NPpNEGT 103 | JhjS1MCtDht5nqAbIpiGTr46NOQwLeGiKwPQ3sF0WzzODjFWPEyt1F+jlrpOMB3LZNjRM8NDczXK 104 | o66SVQKg/QfTR6i3AThOexurjYde6ejIK0NHrEYQNZzUABE4gwTOMWqcWvU4Zu2OHukQgTRsFlDU 105 | 38OiFtys3PsOpI8SPK9TOL/gQRH7UQaUxay8ov4uXQ+0MdcpDDMWeqC+GqV4NpsND8o2N5IKoA2T 106 | vEjweDbXscvofKEQGpTJ9JcGIAHg0WCTCiIpABIIHukgEh6gjTlPcYxaxisTXYPA6ed8NhsaEH1O 107 | JLwVFo9jwiwcPICvtVx2XAsrEWFLTgUvm8fPiVuHUE/v7j3tE1cvvSJqHYQdwsqLhFjncV3iqcZN 108 | 08yuLG/6vdMvabHxSVEXGxucVt6v+OUVZleyb4hGWbKphTW2trFoPFE1zVw2w5bn59jq0gLL53JV 109 | w1r9sVyXAavh6ymckEMYDV3Y13reqSIBTlv3NtaxbQdLNDaxSIP5/xPCIGxzeydriMYYgKLHQJwW 110 | pY2GsnkaykacJuR1fOGGMLesrtYt3ayls8sVfS/MTrP5mdtO0xLSKhPOCkskWJpaittkD4cjrLtv 111 | wDV4QA1ARJpI24G0luvmIAnvowoHEM0XjvCqqQTPzgFWaZLMmy7iIc2uvn6CiF+lTurmpOxO4vLX 112 | 1kmunHFheVFUrt11DR6zSTJn0UrRYokkQeSoJ+ov19FJMTyNKxRApJlDvNpp6+4xtbB409bHA0St 113 | WxzNrbjrqC+HV9dCAURd/CCPYjC8wET3SprIQovFYlzZ8daRKzMXIgkDEDZMqb5cw1dHzw4XVGUv 114 | ic4dO1lhPW8v0kbo/nJdeeJ6HkcYgHD0hkc76Hmw3uO1NMTiLNXUzIoca0SRCMM/ixAiDECkzQEe 115 | jTa2eDd0GcvX2tXD1tcytiGiM2cBQEZlOr0vFgu2lYqepxYmu9W6xJKp0mo1IGJEhVXhqavVtN0O 116 | J0wPRBuOA3Yr7yc8WlkTNIwBHjsQ8dRVy8/rT2EAIsXYnkDXcs3HakPFqReCYC60nstajWa7rlYT 117 | djucSADZrnvMZGfddoIcEWLJ/z8aUlxfp57IMkQcuXkfRWqAvFeneY5FMu0LeXceAzHPrfYhAoBq 118 | r+OP5VDIrfGuEX0sLb+/CADyqQUAEc8akU/FrZhtAFBF1dT4h7JlJjpEUgOUXV2pMQXmyWfo0deK 119 | QhAVYJnZWCOqmJZPP0gNUJ6GCb8lZ2J1lcx7mwuNftdJn7/UAK0uLurr6sv1yvxd03w31oj8h920 120 | oJsEkBqgQmGdTk/4BxHgKdDajxWBeS/iGpHUAKHhli30AFYamCfM4p0ZW9FKa0R8j4DYysfNwNID 121 | hB4IR2+8ljWawFsZvozlKtCcCSvWooj0AKEh7t6e8rw9ZiducOdpY8+MOw+3IioBEI4kL96ddUtn 122 | puksTE+xqua7WQoCmfVKAIT2mqNeqFZn2/U8YNhy0vvo0xLhWhmA0BgzE+NsrYbzIcx7pm9cE6Hd 123 | XSujUgDBrL/1/ntsqQbD2RJZXBOXz1s2211rQZ8TMvcm4HMBXc+eXopx99ZNmqMsso7tfSxMT7A7 124 | EazzoNfhsbic5FsvcdUDCJoniFaXl9j46Fk6BLiVtdDD73ZBAjiYLM/Tn9XFwnppdDfLoSZAZYhC 125 | kSj1RpMlCFKt7XT4sJ0lyE9QJZgACqwrLE7aWWV2s8HqLS11AaKWCNFhs0gsUXrgHXMY/EEAkP5R 126 | VHy3trqsdE8DHWwmSgMEhQCiMDmKwgqwJlpPo90Hn5U1oJQVVkkN4UgDC9NJ0kDsayAAqKyzEkQN 127 | 3h+Btt9k9RUjAEjXHhjKQtQbBWJdAwFABl1FaCgLIDIopcpt8O+mUw5OsibIl1CEztRH4d9Hd5wd 128 | Vlhuba1kxmPLIpANDSgPEFzfNXd0shR58ajmBqZ0xr1MTZ4sNpj8qi8iQh3KAqSB09TeYdu7Knz/ 129 | tJHTKqxgq74arSRAGKo6ybm4U+cLWHAESFjFxn6YikObcpPoJLlb6d454Bie8mhW+oAfoG2795RA 130 | 0n+vwrVSAMHd3RbyXYjhy21Bb7T1nntZY1uH20nXdXrKAIThCq5+ay2d5Cc6Gk/WOpu6SV8JgNDj 131 | 1KrnMbZkhBYit+66t7THZvxNxnslAIKZXs1Ed7th0dul2rfQkXfdQpLbmdRJetIDBHDceiuPnTbD 132 | y1fWmXAvQ7JTxVJY6QHCQ2J+CcDNZLw/1OhlfaUHCCvMfkn71m0sl8+znEtvNvSrHtXylRogzEW8 133 | nPsYFQ0nn0l6u2Emm5UWIqkBSjW3GNvU83tt/pWljVgXXo3pefnNMpQaoEgdPCAWo9cQQmCRrayu 134 | SgeR1AD5OXxp/7kx3aIiIFqlSbVM5r3UAGmNWE+fGMbQE8kiAUA+tCQgksW8DwDyASBkCfMeE2vR 135 | RWqA6sHqWSdQKskaAST6GpHUAPnh2s4Iy+pydSefWCNapyPToorUAGXIM5nfsjw/Z1oEWGb10Fua 136 | FnSTAFIDBI9k8Ankl2D4WrLgJbZk3pNlJqJ5LzVAAMdKD1ArwO7evmk56UJ5oVE0iKQHqBbeyKxS 137 | MTM5bjVoKVzJvKc5kUgiPUB52gn30kOr1viAZ43jUY48DXsirRFJDxAadGFm2tO5EOY+t268r7Fk 138 | +xNrRKKIEgBhIg0PrV7J9dFzrNr6j1fl8CIfJQCCImGR3ZmaqLlOx98dtWR51bwgHmWgDEDQJyyy 139 | 2+NjNVMt4IEHWJVEKYDQsOiJbly+4OoQg+Hq2rl3lIMH+lTybDzOSqDBt+zoZe3d26AHbsFaz+S1 140 | d10FkrswPkRUEiDoOZFKlRoe1tLWnfewlo4uFmmwpg70OAt3pkuWFo+p7kM71yxLaxqrWfb+JtxM 141 | 59gX5+6w8SujVJDR0vkxPATfVOEoELYl4KB8YXba34LXUe5KAxQij/WAaIHenVHyPE9gAI5bjH8N 142 | p47a1pOiKDeJNmoVEDXR2TF8BmJfA8oDBJVh7oOeKIAoAMi+BsoxAFGKnE8FYk8DQQ+k01cskWSN 143 | dXAYUVekur8MADI0ESCCq1/Io/RmHvwFUlkDSltherU8MX2TPXR3hv5m2b2L8/qfPry+RL4QTza2 144 | sBP0Sqh/tKrlyu5DJRgulAfo2ckb7OvXLrOejLnz8E+Rg3H8fXlmik2Sq99jW3rY77ucrWQb2kO4 145 | W2UBQm/z4wvvWAJns1bdTs7GfzB5nR0imL7X9wl2knolFUXJOdALV86xX719nBsePSgA6Q9XL7If 146 | Tozpv1bmWrke6EcXTrNnb7r/cBmGtWZ6cO371BupJEr1QLWCRwPmC7TB+vPxq9qtEp/KAPTFG1dr 147 | 0vMYKQFEXyGLThVRAqBPkll++Mp5z9oUk+s9dXAq1osKKwHQCx7CozXaixNq7OhLDxDM9Qfpz2vZ 148 | t7LEHp6b9Tpbz/OTHiDMffySr87eIs8b4pzx4tGT1AA153Pss9NTPHpxJc5TtI+WohXuInkkk1Wk 149 | BgjDl9+ynybTa2tyOdbU61RqgCptiuoVUOvr+7PkUJM8b+QIInzKJlID9DDtrPstj5XNeQxj+Zz4 150 | PhGN+pQaIGNl/b7HhFo2iAKAPKZqnSb2MllmAUAeA4Ts0AvJYpkFAPkAkEyTaqkBulIHD8hfoGes 151 | NxP4QoR5L7qIBNB1u8q+0tRqN4rr4S/SO8MqSckyowfSjBIOh4VZvhYGIPqPHTMq2uz+NL341m8Z 152 | SW6c8KhUDkyojZPqUDiyVCl8vX0vDEChUPiMXeXdpFMU7zb71wuNUu/zQTRmWuw89UJ6f9bhUHjM 153 | NFKdBBAIIGYbIOj45b5dvqn6NzZ6wBxBpFlmoUhozLdC28xYGIDodRJcAP1t+042lUjZVIvz4BP0 154 | uvG/tNh4YzS2O3I0H6LPSCjysvMSeJOCMACdem0IANmeSEONv7jvM95oU5fLT7q26+6sXZYm1flc 155 | 4ezxf/7JWgz/QwkDEFRF3jOGeVT2Bh3+e7OrhycqV5zX6IzYq5znxKiO/r8hxkathQKI6nXMRt0+ 156 | EvSnn37Ikwk1Js7f7un9SN52bqKx2Et2wvsdVjivSvsOPDdGSuvnUdw2Opb80si/WHON3s+1GI6w 157 | Z/p3W7K8Nis/WZps7OJZodpEtB4Iw9iRzZRv5TuY9c899jk2qnuTspV4VsKg53l8133c8CAPekX4 158 | m1byqqcwwgFE7y8ZIgVu7j7DgmZXaGvhGw8/zn5nw8Q2S/a3lNbztFywQD0Qt5CLPVr/+SZ3fJ8i 159 | OqixPyWeGruU6d29B5tIB3lLkKfFvRHyrDFMJnMfvbe0lx6x4JETtMr8nZ4+9kdyj5el4ceJxBPJ 160 | /1w+feJnTtLwI65Q461eQU7mQlo6mdVlNjd9i92fzbCv0fPT+1eX2A56PVQ1wfrOSLKJYZGw2j5X 161 | tTSMv0UiDflr59+JGr8X4V5Y5wo0FzpE+2OvO1FygnqQ1s4udpFc+2qWUy89q9NLEAEmvQCaDwge 162 | K1sT+nhWruONqe9aCVePYYTtgaDMfQfSR2np9ltOFYs3K88TRH5IPJU6c+XtEw/6kbcbeQoNEBSw 163 | 78DnaYU69IBTZcDZ+EoF13ZO064UvyEaXb3639Pe77NUKhDH985mfhwZuh0lmw0PUprcVplWnpb2 164 | TpZs9M7NbygcLsaaEnu0/EX9FB6gM8NDc4VCaJAawDFEmA95ARHgSaaavjT61lvXRQVHK7fwAKEi 165 | 2Gh1E6JYlacINcXxfn4Iz6njwmyYVqurFAChgm5C1EYbrw0x8wfBqil2s99kgwd1FH4SbWyovYPp 166 | tni8MOx0Yo13uN+5PcnytNDohmDCjDmPDMOWXh/SAaRVzg0THxBNkx9p7UlBLW27n6Kb6tXqK80Q 167 | Zqzkib8PHabFxifpe+6JKp2OYB3d2xkNPcbkLd1jhTnV0vKCyOs8ZhWVtgfSV3z/wfQRWrU+TN9x 168 | PWGfo2EMw5nVnghznXg8+ebl0yOD+nLIeC3cZipPI3zw3qXhrr49v45GQziEdQ/9tdlJJxKJsBi9 169 | 2gCvu6wm6HFouPprrDH2zKWT//5ltbCy/KZED2RsrEefTqdpVEpTr5Sm3yz3SptteaC3IbiuNTRE 170 | /3zx1PEXjXnJfq8kQPpGfeSp9F6CaZAex9lLk+YBmjc9of/deJ1ZWTq3urSYCkUi5+j5nZdHJVnP 171 | MdYzuHdBAzR3GsCygAtJBUkEGgg0sJkG/gd93k1L52vFKQAAAABJRU5ErkJggg== 172 | --00000000000044b4a00626f4fcab 173 | Content-Type: image/png; name="warning_triangle.png" 174 | Content-Disposition: attachment; filename="warning_triangle.png" 175 | Content-Transfer-Encoding: base64 176 | Content-ID: 177 | 178 | iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAYxJREFUSA3t 179 | VLFOhEAUBKLFXXMd3VlqZXGJrX6AP+AHWFsZ4CpDoQmBhMTyfkdbEwsrLaiJnQ1XmIAzCuTt3nKs 180 | xk432fB2dmYeb9+C4/ypEQTBKed3ivZsyavVatd13ZyTsa3OOkFRFBdN0xxwMrZN4NoQ4zj2q6p6 181 | gfmMfFTxNp1O94G/jumtKoD5TWdOQ8bExsy5P1rBcrlc1HX9AFPlZVBF7XneUZIkj9sSKSITEea3 182 | ujl5xLhn0khsawJcyTMYHUuBjLlHjsT0eDBBnucTkDMpyLLM5ZQYOS1Xg7+WgwnKsoxAmRtVKjhv 183 | uSrarowJoijaQ/mhUWEAyaXGsOUYE0CQgswjsh2TVrPB18/TQdPY1LsNph1wgh7dS6pSAb5MD/d7 184 | 9OpJAxlTSw+JKYv1en2OUheSIOOBW9RTqKVHDyDoE6BJM3w413LzJzE96NVpd7oA2a8Q+93a9ER/ 185 | GhOuYX7rdUn8s8lhGPI3/IS19X9eM9WX7+jHYZqmz90R5b9ozmR8UXr+j/ET+ADSfKckAihanAAA 186 | AABJRU5ErkJggg== 187 | --00000000000044b4a00626f4fcab-- 188 | --0000000000004498f20626f4fc34 189 | Content-Type: message/delivery-status 190 | 191 | Reporting-MTA: dns; googlemail.com 192 | Arrival-Date: Fri, 15 Nov 2024 07:11:45 -0800 (PST) 193 | X-Original-Message-ID: 194 | 195 | Final-Recipient: rfc822; test@tozzi.app 196 | Action: failed 197 | Status: 5.7.0 198 | Remote-MTA: dns; mx01.ionos.it. (217.72.192.00, the server for the domain tozzi.app.) 199 | Diagnostic-Code: smtp; 550-Requested action not taken: mailbox unavailable 200 | 550 For explanation visit https://postmaster.1und1.de/en/case?c=r1601&i=ip&v=209.85.167.49&r=1N7PDv-1tqRg71qLV-014hm4 201 | Last-Attempt-Date: Fri, 15 Nov 2024 07:11:46 -0800 (PST) 202 | 203 | --0000000000004498f20626f4fc34 204 | Content-Type: message/rfc822 205 | 206 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 207 | d=gmail.com; s=20230601; t=1731683506; x=1732288306; darn=tozzi.app; 208 | h=to:subject:message-id:date:from:mime-version:from:to:cc:subject 209 | :date:message-id:reply-to; 210 | bh=MnhsSft+8xtTb8owZckvjRBGf9RWN8tnmwGh1VU2hO8=; 211 | b=lMy7UlT6TOPM++w4mOh36qIWol8m+ic3mqBbn0EPHKLFCx4PmxLQBStT1OcCOhjb2/ 212 | uiF3BMrW9IcyFqH5c6IhLMXzbe0CoYpOJ0Xl8j5DLOZH13uJF3S7LORck95sT8g2raNK 213 | fQ1TLPwdA8wPfeGbmta4z7VLlZdz+3LmfFei0B6pProzr8ZCRpyFbVMb0B9391NO3H7K 214 | zosAdoNXvQTwEOJRKh0VKkM0TnPZbm8Wfssgmhuk7jbbWj3ZjOlMdKFt43vLJo6Yh9d3 215 | OIfzf+2kb+2rJLg3wCPPd6ZkY6PJSBbj230l5sUg804rMD7c0/eeL+3+z4vkyG78DNuQ 216 | boPg== 217 | X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 218 | d=1e100.net; s=20230601; t=1731683506; x=1732288306; 219 | h=to:subject:message-id:date:from:mime-version:x-gm-message-state 220 | :from:to:cc:subject:date:message-id:reply-to; 221 | bh=MnhsSft+8xtTb8owZckvjRBGf9RWN8tnmwGh1VU2hO8=; 222 | b=a0uCRnetGIIWquG9D1sq4Er6oBfgw/jeavvYsGJzPx5cTILIEQH/KsfbW6JczjfBoy 223 | EK3PUpkiOeaWMXcCI4N61NXiv2yB36yu4yYId1/3cUv1jhNDKsJZnQ5hRz6vawn8eMgN 224 | W8iB9GmCixF9W9rfh8/vKGrjZ9TQOAHIfEAqiPnszsGoKVhUybeWiVwFfoo+CaNZWPb+ 225 | UYXBodhuaNdtOHyC950HgPur8n9r33NcmEaCP7WQ2TUm5gNmkI3RHHbOExodrzQOxgrv 226 | 2RaYM2PwRFz957JrS6r7spDs2WpjW49NGaG0pi9U/DzKNLzfYCZroaqAV+WvFW5OBdv9 227 | U5tA== 228 | X-Gm-Message-State: AOJu0YyIXYcma0N6NTJhgfEbxTBhCy3bZ/Wym57PN4ckwZ+7g/1//T64 229 | tiYX0TyUWP3zN90IQUwiCh3kHBjzilCMk9T1n3ypB9P4K7IZk7wSo/sW7pSC6KDaHHocNCi6Ij8 230 | Ah/ET6L8DnjyGWT7x0OCylCZOHibo7w== 231 | X-Google-Smtp-Source: AGHT+IHrk16d3H2ImmIeQYsVS2mowKSeiNqbcjeZn1XnfKXfhbHfuno3t3lRb9VQVyeDKQTK/nFfUcaPjm7GV0mXePw= 232 | X-Received: by 2002:a05:6512:28b:b0:53d:abc8:b6d3 with SMTP id 233 | 2adb3069b0e04-53dabc8b71cmr1231628e87.12.1731683505523; Fri, 15 Nov 2024 234 | 07:11:45 -0800 (PST) 235 | MIME-Version: 1.0 236 | From: Biagio Tozzi 237 | Date: Fri, 15 Nov 2024 16:11:35 +0100 238 | Message-ID: 239 | Subject: Test Delivery Status 240 | To: test@tozzi.app 241 | Content-Type: multipart/alternative; boundary="00000000000035d3f30626f4fcf9" 242 | 243 | --00000000000035d3f30626f4fcf9 244 | Content-Type: text/plain; charset="UTF-8" 245 | 246 | !!! 247 | 248 | --00000000000035d3f30626f4fcf9 249 | Content-Type: text/html; charset="UTF-8" 250 | 251 |
!!!
252 | 253 | --00000000000035d3f30626f4fcf9-- 254 | 255 | --0000000000004498f20626f4fc34-- 256 | -------------------------------------------------------------------------------- /src/test/resources/Test - Simple Mail (2).eml: -------------------------------------------------------------------------------- 1 | Delivered-To: biagio.tozzi@gmail.com 2 | Return-Path: 3 | MIME-Version: 1.0 4 | From: Tozzi APP 5 | Date: Fri, 15 Nov 2024 15:39:46 +0100 6 | Message-ID: 7 | Subject: Test - Simple Mail (2) 8 | To: Biagio Tozzi 9 | Content-Type: multipart/mixed; boundary="0000000000007d35900626f48a39" 10 | 11 | --0000000000007d35900626f48a39 12 | Content-Type: multipart/alternative; boundary="0000000000007d358e0626f48a37" 13 | 14 | --0000000000007d358e0626f48a37 15 | Content-Type: text/plain; charset="UTF-8" 16 | 17 | Test - Simple Mail 2! 18 | 19 | --0000000000007d358e0626f48a37 20 | Content-Type: text/html; charset="UTF-8" 21 | 22 |
Test - Simple Mail 2!
23 | 24 | --0000000000007d358e0626f48a37-- 25 | --0000000000007d35900626f48a39 26 | Content-Type: image/png; name="640px-Flag_of_Italy.svg.png" 27 | Content-Disposition: attachment; filename="640px-Flag_of_Italy.svg.png" 28 | Content-Transfer-Encoding: base64 29 | Content-ID: 30 | X-Attachment-Id: f_m3iui2v40 31 | 32 | iVBORw0KGgoAAAANSUhEUgAAAoAAAAGrBAMAAACiYDt4AAAABGdBTUEAALGPC/xhBQAAACBjSFJN 33 | AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAHlBMVEUAkkaLzavA5NHDt6zJ 34 | ZGYAkkar28L////vuLzOKzek0kluAAAABXRSTlOr0ePp9iuyB78AAAABYktHRAcWYYjrAAAAB3RJ 35 | TUUH6AkCFQwOwDqaJQAAAoFJREFUeNrt0EEVABAABTCqyCCCClSRQQRttfgHb4uwMmPWjjk3pggU 36 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 37 | QIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIEC 38 | BQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoU 39 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 40 | QIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIEC 41 | BQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoU 42 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 43 | QIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIEC 44 | BQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoU 45 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 46 | QIECBQoUKFCgQIECBQoUKFCgQIECBX4TmFNbTB8xD3hnp3V7jVeRAAAAJXRFWHRkYXRlOmNyZWF0 47 | ZQAyMDI0LTA5LTAyVDIxOjEyOjE0KzAwOjAwyLh7FAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNC0w 48 | OS0wMlQyMToxMjoxNCswMDowMLnlw6gAAAAASUVORK5CYII= 49 | --0000000000007d35900626f48a39-- 50 | -------------------------------------------------------------------------------- /src/test/resources/Test - Simple Mail (3).eml: -------------------------------------------------------------------------------- 1 | Delivered-To: test@tozzi.app 2 | Return-Path: 3 | MIME-Version: 1.0 4 | References: 5 | In-Reply-To: 6 | From: Biagio Tozzi 7 | Date: Fri, 15 Nov 2024 15:48:22 +0100 8 | Message-ID: 9 | Subject: Re: Test - Simple Mail (2) 10 | To: Tozzi APP 11 | Cc: test2@tozzi.app 12 | Content-Type: multipart/alternative; boundary="0000000000003a27910626f4a973" 13 | 14 | --0000000000003a27910626f4a973 15 | Content-Type: text/plain; charset="UTF-8" 16 | 17 | Reply! 18 | 19 | 20 | Il giorno ven 15 nov 2024 alle ore 15:39 Tozzi APP ha 21 | scritto: 22 | 23 | > Test - Simple Mail 2! 24 | > 25 | 26 | --0000000000003a27910626f4a973 27 | Content-Type: text/html; charset="UTF-8" 28 | Content-Transfer-Encoding: quoted-printable 29 | 30 |
Reply!

Il giorno= 33 | ven 15 nov 2024 alle ore 15:39 Tozzi APP <test@tozzi.app> ha scritto:
44 |
45 | 46 | --0000000000003a27910626f4a973-- 47 | -------------------------------------------------------------------------------- /src/test/resources/Test - Simple Mail.eml: -------------------------------------------------------------------------------- 1 | Delivered-To: biagio.tozzi@gmail.com 2 | Return-Path: 3 | MIME-Version: 1.0 4 | From: Tozzi APP 5 | Date: Fri, 15 Nov 2024 14:58:07 +0100 6 | Message-ID: 7 | Subject: Test - Simple Mail 8 | To: Biagio Tozzi 9 | Content-Type: multipart/related; boundary="0000000000008bcac00626f3f5b4" 10 | 11 | --0000000000008bcac00626f3f5b4 12 | Content-Type: multipart/alternative; boundary="0000000000008bcabe0626f3f5b3" 13 | 14 | --0000000000008bcabe0626f3f5b3 15 | Content-Type: text/plain; charset="UTF-8" 16 | 17 | Simple Mail! 18 | [image: 640px-Flag_of_Italy.svg.png] 19 | 20 | --0000000000008bcabe0626f3f5b3 21 | Content-Type: text/html; charset="UTF-8" 22 | 23 |
Simple Mail!
640px-Flag_of_Italy.svg.png
24 | 25 | --0000000000008bcabe0626f3f5b3-- 26 | --0000000000008bcac00626f3f5b4 27 | Content-Type: image/png; name="640px-Flag_of_Italy.svg.png" 28 | Content-Disposition: inline; filename="640px-Flag_of_Italy.svg.png" 29 | Content-Transfer-Encoding: base64 30 | Content-ID: 31 | X-Attachment-Id: ii_m3it0i7x0 32 | 33 | iVBORw0KGgoAAAANSUhEUgAAAoAAAAGrBAMAAACiYDt4AAAABGdBTUEAALGPC/xhBQAAACBjSFJN 34 | AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAHlBMVEUAkkaLzavA5NHDt6zJ 35 | ZGYAkkar28L////vuLzOKzek0kluAAAABXRSTlOr0ePp9iuyB78AAAABYktHRAcWYYjrAAAAB3RJ 36 | TUUH6AkCFQwOwDqaJQAAAoFJREFUeNrt0EEVABAABTCqyCCCClSRQQRttfgHb4uwMmPWjjk3pggU 37 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 38 | QIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIEC 39 | BQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoU 40 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 41 | QIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIEC 42 | BQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoU 43 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 44 | QIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIEC 45 | BQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoU 46 | KFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCg 47 | QIECBQoUKFCgQIECBQoUKFCgQIECBX4TmFNbTB8xD3hnp3V7jVeRAAAAJXRFWHRkYXRlOmNyZWF0 48 | ZQAyMDI0LTA5LTAyVDIxOjEyOjE0KzAwOjAwyLh7FAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNC0w 49 | OS0wMlQyMToxMjoxNCswMDowMLnlw6gAAAAASUVORK5CYII= 50 | --0000000000008bcac00626f3f5b4-- 51 | -------------------------------------------------------------------------------- /src/test/resources/accettazione.eml: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | Delivered-To: sender@fakepec.it 3 | Subject: ACCETTAZIONE: Test PEC 4 | X-Riferimento-Message-ID: 5 | Date: Fri, 15 Nov 2024 18:20:38 +0100 6 | To: sender@fakepec.it 7 | X-Ricevuta: accettazione 8 | From: posta-certificata@fakepec.it 9 | MIME-Version: 1.0 10 | Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----76F9CFD0D4B5B34499C167119D5A1AEC" 11 | Message-ID: 12 | 13 | This is an S/MIME signed message 14 | 15 | ------76F9CFD0D4B5B34499C167119D5A1AEC 16 | Content-Type: multipart/mixed; boundary="----------=_1731691238-288127-3078" 17 | Content-Transfer-Encoding: binary 18 | MIME-Version: 1.0 19 | 20 | ------------=_1731691238-288127-3078 21 | Content-Type: multipart/alternative; 22 | boundary="----------=_1731691238-288127-3079" 23 | Content-Transfer-Encoding: binary 24 | 25 | ------------=_1731691238-288127-3079 26 | Content-Type: text/plain; charset="iso-8859-1" 27 | Content-Disposition: inline 28 | Content-Transfer-Encoding: quoted-printable 29 | 30 | -- Ricevuta di accettazione del messaggio indirizzato a rec@fakepec.= 31 | it ("posta certificata") -- 32 | 33 | Il giorno 15/11/2024 alle ore 18:20:38 (+0100) il messaggio con Oggetto 34 | "Test PEC" inviato da "sender@fakepec.it" 35 | ed indirizzato a: 36 | rec@fakepec.it ("posta certificata") 37 | =E8 stato accettato dal sistema ed inoltrato. 38 | Identificativo del messaggio: opec210312.20241115182038.288127.606.1.53@pec= 39 | .fakepec.it 40 | L'allegato daticert.xml contiene informazioni di servizio sulla trasmissione 41 | 42 | ------------=_1731691238-288127-3079 43 | Content-Type: text/html; charset="iso-8859-1" 44 | Content-Disposition: inline 45 | Content-Transfer-Encoding: quoted-printable 46 | 47 | 48 | Ricevuta di accettazione 49 | 50 |

Ricevuta di accettazione

51 |

52 | Il giorno 15/11/2024 alle ore 18:20:38 (+0100) il messaggio
53 | "Test PEC" proveniente da "sender@fakepec.it"
54 | ed indirizzato a:
55 | rec@fakepec.it ("posta certificata") 56 |

57 | Il messaggio è stato accettato dal sistema ed inoltrato.
58 | Identificativo messaggio: opec210312.20241115182038.288127.606.1.53@fakepec= 59 | .it
60 | 61 | 62 | 63 | ------------=_1731691238-288127-3079-- 64 | 65 | ------------=_1731691238-288127-3078 66 | Content-Type: application/xml; name="daticert.xml" 67 | Content-Disposition: inline; filename="daticert.xml" 68 | Content-Transfer-Encoding: base64 69 | 70 | PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHBvc3RhY2VydCB0aXBvPSJhY2NldHRhemlvbmUiIGVycm9yZT0ibmVzc3VubyI+CiAgICA8aW50ZXN0YXppb25lPgogICAgICAgIDxtaXR0ZW50ZT5zZW5kZXJAZmFrZXBlYy5pdDwvbWl0dGVudGU+CiAgICAgICAgPGRlc3RpbmF0YXJpIHRpcG89ImNlcnRpZmljYXRvIj5yZWNAZmFrZXBlYy5pdDwvZGVzdGluYXRhcmk+CiAgICAgICAgPHJpc3Bvc3RlPnNlbmRlckBmYWtlcGVjLml0PC9yaXNwb3N0ZT4KICAgICAgICA8b2dnZXR0bz5UZXN0IFBFQzwvb2dnZXR0bz4KICAgIDwvaW50ZXN0YXppb25lPgogICAgPGRhdGk+CiAgICAgICAgPGdlc3RvcmUtZW1pdHRlbnRlPkZBS0VQRUMgUEVDIFMucC5BLjwvZ2VzdG9yZS1lbWl0dGVudGU+CiAgICAgICAgPGRhdGEgem9uYT0iKzAxMDAiPgogICAgICAgICAgICA8Z2lvcm5vPjE1LzExLzIwMjQ8L2dpb3Jubz4KICAgICAgICAgICAgPG9yYT4xODoyMDozODwvb3JhPgogICAgICAgIDwvZGF0YT4KICAgICAgICA8aWRlbnRpZmljYXRpdm8+b3BlYzIxMDMxMi4yMDI0MTExNTE4MjAzOC4yODgxMjcuNjA2LjEuNTNAZmFrZXBlYy5pdDwvaWRlbnRpZmljYXRpdm8+CiAgICAgICAgPG1zZ2lkPiZsdDtTTjA1SUUkOTUxREVDMTZDMUNGRDNFNEZEOEZGMUIxRDI0QTk5QUVAZmFrZXBlYy5pdCZndDs8L21zZ2lkPgogICAgPC9kYXRpPgo8L3Bvc3RhY2VydD4K 71 | 72 | ------------=_1731691238-288127-3078-- 73 | 74 | -------------------------------------------------------------------------------- /src/test/resources/consegna.eml: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | Delivered-To: sender@fakepec.it 3 | Subject: AVVISO DI MANCATA CONSEGNA: Test PEC 4 | X-Riferimento-Message-ID: 5 | Date: Fri, 15 Nov 2024 18:21:03 +0100 6 | Message-ID: 7 | To: sender@fakepec.it 8 | X-Ricevuta: errore-consegna 9 | From: posta-certificata@fakepec.it 10 | MIME-Version: 1.0 11 | Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----C5739FE382D0724A2D3DE14402B23D4D" 12 | 13 | This is an S/MIME signed message 14 | 15 | ------C5739FE382D0724A2D3DE14402B23D4D 16 | Content-Type: multipart/mixed; boundary="----------=_1731691263-186549-1172" 17 | Content-Transfer-Encoding: binary 18 | MIME-Version: 1.0 19 | 20 | ------------=_1731691263-186549-1172 21 | Content-Type: multipart/alternative; 22 | boundary="----------=_1731691263-186549-1173" 23 | Content-Transfer-Encoding: binary 24 | 25 | ------------=_1731691263-186549-1173 26 | Content-Type: text/plain; charset="iso-8859-1" 27 | Content-Disposition: inline 28 | Content-Transfer-Encoding: quoted-printable 29 | 30 | --Avviso di mancata consegna del messaggio-- 31 | 32 | Il giorno 15/11/2024 alle ore 18:21:03 (+0100) nel messaggio con Oggetto 33 | "Test PEC" inviato da "sender@fakepec.it" 34 | e destinato all'utente 35 | "rec@fakepec.it" 36 | =E8 stato rilevato il seguente errore: 37 | 5.1.1 - FAKE Pec S.p.A. - indirizzo non valido 38 | Il messaggio =E8 stato rifiutato dal sistema. 39 | Identificativo del messaggio: opec210312.20241115182038.288127.606.1.53@fakepec= 40 | .it 41 | 42 | ------------=_1731691263-186549-1173 43 | Content-Type: text/html; charset="iso-8859-1" 44 | Content-Disposition: inline 45 | Content-Transfer-Encoding: quoted-printable 46 | 47 | 48 | Avviso di mancata consegna 49 | 50 |

Avviso di mancata consegna

51 |

52 | Il giorno 15/11/2024 alle ore 18:21:03 (+0100) nel messaggio
53 | "Test PEC" proveniente da "sender@fakepec.it"
54 | e destinato all'utente "rec@fakepec.it"
55 | è stato rilevato un errore: 5.1.1 - FAKE Pec S.p.A. - indirizzo = 56 | non valido
57 | Il messaggio è stato rifiutato dal sistema.
58 | Identificativo messaggio: opec210312.20241115182038.288127.606.1.53@fakepec= 59 | .it
60 | 61 | 62 | 63 | ------------=_1731691263-186549-1173-- 64 | 65 | ------------=_1731691263-186549-1172 66 | Content-Type: application/xml; name="daticert.xml" 67 | Content-Disposition: inline; filename="daticert.xml" 68 | Content-Transfer-Encoding: base64 69 | 70 | PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHBvc3RhY2VydCB0aXBvPSJlcnJvcmUtY29uc2VnbmEiIGVycm9yZT0ibm8tZGVzdCI+CiAgICA8aW50ZXN0YXppb25lPgogICAgICAgIDxtaXR0ZW50ZT5zZW5kZXJAZmFrZXBlYy5pdDwvbWl0dGVudGU+CiAgICAgICAgPGRlc3RpbmF0YXJpIHRpcG89ImNlcnRpZmljYXRvIj5yZWNAcGVjLml0PC9kZXN0aW5hdGFyaT4KICAgICAgICA8cmlzcG9zdGU+c2VuZGVyQGZha2VwZWMuaXQ8L3Jpc3Bvc3RlPmEKICAgICAgICA8b2dnZXR0bz5UZXN0IFBFQzwvb2dnZXR0bz4KICAgIDwvaW50ZXN0YXppb25lPgogICAgPGRhdGk+CiAgICAgICAgPGdlc3RvcmUtZW1pdHRlbnRlPkZBS0UgUEVDIFMucC5BLjwvZ2VzdG9yZS1lbWl0dGVudGU+CiAgICAgICAgPGRhdGEgem9uYT0iKzAxMDAiPgogICAgICAgICAgICA8Z2lvcm5vPjE1LzExLzIwMjQ8L2dpb3Jubz4KICAgICAgICAgICAgPG9yYT4xODoyMTowMzwvb3JhPgogICAgICAgIDwvZGF0YT4KICAgICAgICA8aWRlbnRpZmljYXRpdm8+b3BlYzIxMDMxMi4yMDI0MTExNTE4MjAzOC4yODgxMjcuNjA2LjEuNTNAZmFrZXBlYy5pdDwvaWRlbnRpZmljYXRpdm8+CiAgICAgICAgPG1zZ2lkPiZsdDtTTjA1SUUkOTUxREVDMTZDMUNGRDNFNEZEOEZGMUIxRDI0QTk5QUVAZmFrZXBlYy5pdCZndDs8L21zZ2lkPgogICAgICAgIDxjb25zZWduYT5yZWNAZmFrZXBlYy5pdDwvY29uc2VnbmE+CiAgICAgICAgPGVycm9yZS1lc3Rlc28+NS4xLjEgLSBGQUtFIFBlYyBTLnAuQS4gLSBpbmRpcml6em8gbm9uIHZhbGlkbzwvZXJyb3JlLWVzdGVzbz4KICAgIDwvZGF0aT4KPC9wb3N0YWNlcnQ+Cg== 71 | 72 | ------------=_1731691263-186549-1172-- 73 | 74 | --------------------------------------------------------------------------------