├── .github ├── dependabot.yml └── workflows │ ├── pr_workflow.yml │ ├── release.yml │ ├── test_and_build.yml │ └── update-gradle-wrapper.yml ├── .gitignore ├── .idea └── copyright │ ├── Apache_2_0.xml │ └── profiles_settings.xml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── configure-plugin.png ├── list-plugin.png └── successful-notification.png ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── tw │ │ └── go │ │ └── plugin │ │ ├── BuildState.java │ │ ├── EmailNotificationPluginImpl.java │ │ ├── Filter.java │ │ ├── FilterConverter.java │ │ ├── PluginSettings.java │ │ ├── SMTPAuthenticator.java │ │ ├── SMTPMailSender.java │ │ ├── SMTPSettings.java │ │ ├── SessionFactory.java │ │ ├── SessionWrapper.java │ │ └── util │ │ ├── FieldValidator.java │ │ └── JSONUtils.java └── resources │ └── plugin-settings.template.html └── test └── java └── com └── tw └── go └── plugin ├── EmailNotificationPluginImplUnitTest.java ├── FilterConverterTest.java ├── FilterTest.java └── SMTPMailSenderTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: gradle 12 | directory: / 13 | schedule: 14 | interval: monthly 15 | groups: 16 | gradle-deps: 17 | patterns: 18 | - "*" -------------------------------------------------------------------------------- /.github/workflows/pr_workflow.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Testing For PRs 5 | 6 | on: [ pull_request ] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Harden the runner (Audit all outbound calls) 16 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 17 | with: 18 | egress-policy: audit 19 | 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - name: Set up JDK 22 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 23 | with: 24 | java-version: 17 25 | distribution: temurin 26 | - name: Build with Gradle 27 | run: ./gradlew assemble check 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Create Stable Release 5 | 6 | # Controls when the action will run. Workflow runs when manually triggered using the UI 7 | # or API. 8 | on: 9 | workflow_dispatch: 10 | # Inputs the workflow accepts. 11 | inputs: 12 | prerelease: 13 | description: 'The release should be an experimental release' 14 | default: 'NO' 15 | required: true 16 | 17 | jobs: 18 | build_and_release: 19 | runs-on: ubuntu-latest 20 | env: 21 | GITHUB_USER: "gocd-contrib" 22 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 23 | PRERELEASE: "${{ github.event.inputs.prerelease }}" 24 | steps: 25 | - name: Harden the runner (Audit all outbound calls) 26 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 31 | with: 32 | fetch-depth: 0 33 | - name: Set up JDK 34 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 35 | with: 36 | java-version: 17 37 | distribution: temurin 38 | - name: Release 39 | run: ./gradlew verifyExpRelease githubRelease 40 | -------------------------------------------------------------------------------- /.github/workflows/test_and_build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Test and Build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Harden the runner (Audit all outbound calls) 15 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 16 | with: 17 | egress-policy: audit 18 | 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | - name: Set up JDK 21 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 22 | with: 23 | java-version: 17 24 | distribution: temurin 25 | - name: Test with Gradle 26 | run: ./gradlew assemble check 27 | previewGithubRelease: 28 | needs: test 29 | runs-on: ubuntu-latest 30 | env: 31 | GITHUB_USER: "gocd-contrib" 32 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 33 | steps: 34 | - name: Harden the runner (Audit all outbound calls) 35 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 36 | with: 37 | egress-policy: audit 38 | 39 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | fetch-depth: 0 42 | - name: Set up JDK 43 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 44 | with: 45 | java-version: 17 46 | distribution: temurin 47 | - name: Test with Gradle 48 | run: ./gradlew githubRelease 49 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 1 * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Harden the runner (Audit all outbound calls) 14 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 15 | with: 16 | egress-policy: audit 17 | 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - name: Update Gradle Wrapper 21 | uses: gradle-update/update-gradle-wrapper-action@512b1875f3b6270828abfe77b247d5895a2da1e5 # v2.1.0 22 | with: 23 | labels: dependencies 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.iso 17 | *.rar 18 | *.tar 19 | *.zip 20 | 21 | # Logs and databases # 22 | ###################### 23 | *.log 24 | *.sql 25 | *.sqlite 26 | 27 | # OS generated files # 28 | ###################### 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | Thumbs.db 36 | 37 | # Eclipse # 38 | ########## 39 | .classpath 40 | .project 41 | .settings/ 42 | 43 | # IntelliJ # 44 | ########### 45 | .idea/* 46 | out/ 47 | *.ipr 48 | *.iws 49 | *.iml 50 | .gradle 51 | build/ 52 | !.idea/copyright 53 | 54 | # App # 55 | ###### 56 | sample.db 57 | target/ 58 | dist/ 59 | src/main/resources-generated/ 60 | -------------------------------------------------------------------------------- /.idea/copyright/Apache_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [GoCD](https://www.gocd.org) plugin to send email notifications. 2 | 3 | *Usage:* 4 | 5 | ## Installation 6 | 7 | * Place the jar in `/plugins/external` & restart the GoCD Server. 8 | 9 | ## Configuration 10 | 11 | You will see `Email Notification plugin` on the plugin listing page 12 | 13 | ![Plugins listing page][1] 14 | 15 | You will need to configure the plugin: 16 | 17 | ![Configure plugin pop-up][2] 18 | 19 | Please note that 20 | - Enabling "TLS?" enables TLS at transport level. Nothing is exchanged over plaintext and your SMTP server needs to support that. 21 | - If you have a need for "upgrading" to TLS via the `STARTTLS` SMTP protocol command, you will need to enable the Java system property `-Dmail.smtp.starttls.enable=true` on your GoCD server, similar to GoCD's overall server settings documented [here](https://docs.gocd.org/current/configuration/admin_mailhost_info.html#smtps-and-tls). Usually this is associated with port `587`. 22 | 23 | When the stage status changes, you get an email: 24 | 25 | ![Successful Notification][3] 26 | 27 | ## To customize notifications 28 | 29 | * Clone the project 30 | 31 | * Customize when & whom to send emails for different events (Stage - Scheduled, Passed, Failed, Cancelled) 32 | 33 | * Run `./gradlew clean assemble` which will create plugin jar in 'build/libs' folder 34 | 35 | [1]: images/list-plugin.png "List Plugin" 36 | [2]: images/configure-plugin.png "Configure Plugin" 37 | [3]: images/successful-notification.png "Successful Notification" 38 | 39 | ## Contributing 40 | 41 | We encourage you to contribute to GoCD. For information on contributing to this project, please see our [contributor's guide](https://www.gocd.org/contribute/). 42 | A lot of useful information like links to user documentation, design documentation, mailing lists etc. can be found in the [resources](https://www.gocd.org/community/resources.html) section. 43 | 44 | ## License 45 | 46 | ```plain 47 | Copyright 2019 ThoughtWorks, Inc. 48 | 49 | Licensed under the Apache License, Version 2.0 (the "License"); 50 | you may not use this file except in compliance with the License. 51 | You may obtain a copy of the License at 52 | 53 | https://www.apache.org/licenses/LICENSE-2.0 54 | 55 | Unless required by applicable law or agreed to in writing, software 56 | distributed under the License is distributed on an "AS IS" BASIS, 57 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 58 | See the License for the specific language governing permissions and 59 | limitations under the License. 60 | ``` 61 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'java' 18 | apply from: "https://raw.githubusercontent.com/gocd/gocd-plugin-gradle-task-helpers/master/helper.gradle?_=${(int) (new Date().toInstant().epochSecond / 60)}" 19 | 20 | gocdPlugin { 21 | id = 'email.notifier' 22 | pluginVersion = '1.2.0' 23 | goCdVersion = '20.9.0' 24 | name = 'Email Notifier Plugin' 25 | description = 'Plugin to send email notifications' 26 | vendorName = 'ThoughtWorks, Inc.' 27 | vendorUrl = 'https://github.com/gocd-contrib/email-notifier' 28 | 29 | githubRepo { 30 | owner = System.getenv('GITHUB_USER') ?: 'bob' 31 | repo = 'email-notifier' 32 | token = System.getenv('GITHUB_TOKEN') ?: 'bad-token' 33 | } 34 | 35 | pluginProject = project 36 | 37 | prerelease = !"No".equalsIgnoreCase(System.getenv('PRERELEASE')) 38 | assetsToRelease = [project.tasks.jar] 39 | } 40 | 41 | group = 'com.thoughtworks.go' 42 | version = gocdPlugin.fullVersion(project) 43 | 44 | java { 45 | sourceCompatibility = JavaVersion.VERSION_11 46 | targetCompatibility = JavaVersion.VERSION_11 47 | } 48 | 49 | repositories { 50 | mavenCentral() 51 | mavenLocal() 52 | } 53 | 54 | ext { 55 | deps = [ 56 | gocdPluginApi: 'cd.go.plugin:go-plugin-api:25.2.0', 57 | ] 58 | 59 | versions = project.ext.deps.collectEntries { lib, libGav -> [lib, libGav.split(':').last()] } 60 | } 61 | 62 | dependencies { 63 | compileOnly project.deps.gocdPluginApi 64 | implementation 'jakarta.mail:jakarta.mail-api:2.1.3' 65 | implementation 'org.eclipse.angus:smtp:2.0.3' 66 | implementation 'com.google.code.gson:gson:2.13.1' 67 | 68 | testImplementation project.deps.gocdPluginApi 69 | testImplementation platform('org.junit:junit-bom:5.13.0') 70 | testImplementation 'org.junit.jupiter:junit-jupiter-api' 71 | testImplementation 'org.junit.jupiter:junit-jupiter-params' 72 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' 73 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 74 | testImplementation 'org.hamcrest:hamcrest-core:3.0' 75 | testImplementation 'org.mockito:mockito-core:5.18.0' 76 | testImplementation('com.icegreen:greenmail:2.1.3') { 77 | exclude group: 'junit' 78 | } 79 | testImplementation 'org.slf4j:slf4j-simple:2.0.17' 80 | } 81 | 82 | test { 83 | useJUnitPlatform() 84 | } 85 | 86 | jar { 87 | from(configurations.runtimeClasspath) { 88 | into "lib/" 89 | } 90 | 91 | from(sourceSets.main.java) { 92 | into "/" 93 | } 94 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/email-notifier/1e99efd1274b87935551487661181aa92fda84f2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /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= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 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 | -------------------------------------------------------------------------------- /images/configure-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/email-notifier/1e99efd1274b87935551487661181aa92fda84f2/images/configure-plugin.png -------------------------------------------------------------------------------- /images/list-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/email-notifier/1e99efd1274b87935551487661181aa92fda84f2/images/list-plugin.png -------------------------------------------------------------------------------- /images/successful-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/email-notifier/1e99efd1274b87935551487661181aa92fda84f2/images/successful-notification.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | rootProject.name = 'email-notifier' 17 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/BuildState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | 20 | public enum BuildState { 21 | 22 | BUILDING, 23 | FAILING, 24 | PASSED, 25 | FAILED, 26 | CANCELLED, 27 | UNKNOWN; 28 | 29 | public static BuildState fromRawString(String rawString) { 30 | if(rawString == null) { 31 | return null; 32 | } 33 | switch (rawString.toLowerCase()) { 34 | case "building": 35 | return BUILDING; 36 | case "failing": 37 | return FAILING; 38 | case "passed": 39 | return PASSED; 40 | case "failed": 41 | return FAILED; 42 | case "cancelled": 43 | return CANCELLED; 44 | default: 45 | return UNKNOWN; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/EmailNotificationPluginImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import com.google.gson.GsonBuilder; 20 | import com.thoughtworks.go.plugin.api.GoApplicationAccessor; 21 | import com.thoughtworks.go.plugin.api.GoPlugin; 22 | import com.thoughtworks.go.plugin.api.GoPluginIdentifier; 23 | import com.thoughtworks.go.plugin.api.annotation.Extension; 24 | import com.thoughtworks.go.plugin.api.logging.Logger; 25 | import com.thoughtworks.go.plugin.api.request.GoApiRequest; 26 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; 27 | import com.thoughtworks.go.plugin.api.response.GoApiResponse; 28 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 29 | import com.tw.go.plugin.util.FieldValidator; 30 | import com.tw.go.plugin.util.JSONUtils; 31 | 32 | import java.io.IOException; 33 | import java.io.InputStream; 34 | import java.nio.charset.StandardCharsets; 35 | import java.util.*; 36 | 37 | @Extension 38 | public class EmailNotificationPluginImpl implements GoPlugin { 39 | private static final Logger LOGGER = Logger.getLoggerFor(EmailNotificationPluginImpl.class); 40 | 41 | public static final String PLUGIN_ID = "email.notifier"; 42 | public static final String EXTENSION_NAME = "notification"; 43 | private static final List goSupportedVersions = List.of("1.0"); 44 | 45 | public static final String PLUGIN_SETTINGS_SMTP_HOST = "smtp_host"; 46 | public static final String PLUGIN_SETTINGS_SMTP_PORT = "smtp_port"; 47 | public static final String PLUGIN_SETTINGS_IS_TLS = "is_tls"; 48 | public static final String PLUGIN_SETTINGS_SENDER_EMAIL_ID = "sender_email_id"; 49 | public static final String PLUGIN_SETTINGS_SMTP_USERNAME = "smtp_username"; 50 | public static final String PLUGIN_SETTINGS_SENDER_PASSWORD = "sender_password"; 51 | public static final String PLUGIN_SETTINGS_RECEIVER_EMAIL_ID = "receiver_email_id"; 52 | public static final String PLUGIN_SETTINGS_FILTER = "pipeline_stage_filter"; 53 | 54 | public static final String PLUGIN_SETTINGS_GET_CONFIGURATION = "go.plugin-settings.get-configuration"; 55 | public static final String PLUGIN_SETTINGS_GET_VIEW = "go.plugin-settings.get-view"; 56 | public static final String PLUGIN_SETTINGS_VALIDATE_CONFIGURATION = "go.plugin-settings.validate-configuration"; 57 | public static final String REQUEST_NOTIFICATIONS_INTERESTED_IN = "notifications-interested-in"; 58 | public static final String REQUEST_STAGE_STATUS = "stage-status"; 59 | 60 | public static final String GET_PLUGIN_SETTINGS = "go.processor.plugin-settings.get"; 61 | 62 | public static final int SUCCESS_RESPONSE_CODE = 200; 63 | public static final int NOT_FOUND_RESPONSE_CODE = 404; 64 | public static final int INTERNAL_ERROR_RESPONSE_CODE = 500; 65 | 66 | private GoApplicationAccessor goApplicationAccessor; 67 | private SessionFactory sessionFactory; 68 | 69 | @Override 70 | public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationAccessor) { 71 | this.goApplicationAccessor = goApplicationAccessor; 72 | this.sessionFactory = new SessionFactory(); 73 | } 74 | 75 | @Override 76 | public GoPluginApiResponse handle(GoPluginApiRequest goPluginApiRequest) { 77 | String requestName = goPluginApiRequest.requestName(); 78 | if (requestName.equals(PLUGIN_SETTINGS_GET_CONFIGURATION)) { 79 | return handleGetPluginSettingsConfiguration(); 80 | } else if (requestName.equals(PLUGIN_SETTINGS_GET_VIEW)) { 81 | try { 82 | return handleGetPluginSettingsView(); 83 | } catch (IOException e) { 84 | return renderJSON(500, String.format("Failed to find template: %s", e.getMessage())); 85 | } 86 | } else if (requestName.equals(PLUGIN_SETTINGS_VALIDATE_CONFIGURATION)) { 87 | return handleValidatePluginSettingsConfiguration(goPluginApiRequest); 88 | } else if (requestName.equals(REQUEST_NOTIFICATIONS_INTERESTED_IN)) { 89 | return handleNotificationsInterestedIn(); 90 | } else if (requestName.equals(REQUEST_STAGE_STATUS)) { 91 | return handleStageNotification(goPluginApiRequest); 92 | } 93 | return renderJSON(NOT_FOUND_RESPONSE_CODE, null); 94 | } 95 | 96 | @Override 97 | public GoPluginIdentifier pluginIdentifier() { 98 | return getGoPluginIdentifier(); 99 | } 100 | 101 | private GoPluginApiResponse handleNotificationsInterestedIn() { 102 | Map response = new HashMap<>(); 103 | response.put("notifications", Arrays.asList(REQUEST_STAGE_STATUS)); 104 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 105 | } 106 | 107 | private GoPluginApiResponse handleStageNotification(GoPluginApiRequest goPluginApiRequest) { 108 | Map dataMap = (Map) JSONUtils.fromJSON(goPluginApiRequest.requestBody()); 109 | 110 | int responseCode = SUCCESS_RESPONSE_CODE; 111 | Map response = new HashMap<>(); 112 | List messages = new ArrayList<>(); 113 | try { 114 | Map pipelineMap = (Map) dataMap.get("pipeline"); 115 | Map stageMap = (Map) pipelineMap.get("stage"); 116 | 117 | 118 | String pipelineName = (String) pipelineMap.get("name"); 119 | String stageName = (String) stageMap.get("name"); 120 | String stageState = (String) stageMap.get("state"); 121 | 122 | String subject = String.format("%s/%s is/has %s", pipelineName, stageName, stageState); 123 | String body = String.format("State: %s\nResult: %s\nCreate Time: %s\nLast Transition Time: %s", stageState, stageMap.get("result"), stageMap.get("create-time"), stageMap.get("last-transition-time")); 124 | 125 | PluginSettings pluginSettings = getPluginSettings(); 126 | 127 | 128 | boolean matchesFilter = false; 129 | 130 | List filterList = pluginSettings.getFilterList(); 131 | 132 | if(filterList.isEmpty()) { 133 | matchesFilter = true; 134 | } else { 135 | for(Filter filter : filterList) { 136 | if(filter.matches(pipelineName, stageName, stageState)) { 137 | matchesFilter = true; 138 | } 139 | } 140 | } 141 | 142 | if(matchesFilter) { 143 | LOGGER.info("Sending Email for " + subject); 144 | 145 | String receiverEmailIdString = pluginSettings.getReceiverEmailId(); 146 | 147 | String[] receiverEmailIds = new String[]{receiverEmailIdString}; 148 | 149 | if (receiverEmailIdString.contains(",")) { 150 | receiverEmailIds = receiverEmailIdString.split(","); 151 | } 152 | 153 | for (String receiverEmailId : receiverEmailIds) { 154 | SMTPSettings settings = new SMTPSettings(pluginSettings.getSmtpHost(), pluginSettings.getSmtpPort(), pluginSettings.isTls(), pluginSettings.getSenderEmailId(), pluginSettings.getSmtpUsername(), pluginSettings.getSenderPassword()); 155 | new SMTPMailSender(settings, sessionFactory).send(subject, body, receiverEmailId); 156 | } 157 | 158 | LOGGER.info("Successfully delivered an email."); 159 | } else { 160 | LOGGER.info("Skipped email as no filter matched this pipeline/stage/state"); 161 | } 162 | 163 | response.put("status", "success"); 164 | } catch (Exception e) { 165 | LOGGER.warn("Error occurred while trying to deliver an email.", e); 166 | 167 | responseCode = INTERNAL_ERROR_RESPONSE_CODE; 168 | response.put("status", "failure"); 169 | if (!isEmpty(e.getMessage())) { 170 | messages.add(e.getMessage()); 171 | } 172 | } 173 | 174 | if (!messages.isEmpty()) { 175 | response.put("messages", messages); 176 | } 177 | return renderJSON(responseCode, response); 178 | } 179 | 180 | private GoPluginApiResponse handleGetPluginSettingsConfiguration() { 181 | Map response = new HashMap<>(); 182 | response.put(PLUGIN_SETTINGS_SMTP_HOST, createField("SMTP Host", null, true, false, "0")); 183 | response.put(PLUGIN_SETTINGS_SMTP_PORT, createField("SMTP Port", null, true, false, "1")); 184 | response.put(PLUGIN_SETTINGS_IS_TLS, createField("TLS", null, true, false, "2")); 185 | response.put(PLUGIN_SETTINGS_SENDER_EMAIL_ID, createField("Sender Email ID", null, true, false, "3")); 186 | response.put(PLUGIN_SETTINGS_SMTP_USERNAME, createField("SMTP Username", null, false, false, "4")); 187 | response.put(PLUGIN_SETTINGS_SENDER_PASSWORD, createField("Sender Password", null, false, true, "5")); 188 | response.put(PLUGIN_SETTINGS_RECEIVER_EMAIL_ID, createField("Receiver Email-id", null, true, false, "6")); 189 | response.put(PLUGIN_SETTINGS_FILTER, createField("Pipeline/Stage/Status filter", null, false, false, "7")); 190 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 191 | } 192 | 193 | private Map createField(String displayName, String defaultValue, boolean isRequired, boolean isSecure, String displayOrder) { 194 | Map fieldProperties = new HashMap<>(); 195 | fieldProperties.put("display-name", displayName); 196 | fieldProperties.put("default-value", defaultValue); 197 | fieldProperties.put("required", isRequired); 198 | fieldProperties.put("secure", isSecure); 199 | fieldProperties.put("display-order", displayOrder); 200 | return fieldProperties; 201 | } 202 | 203 | private GoPluginApiResponse handleGetPluginSettingsView() throws IOException { 204 | Map response = new HashMap<>(); 205 | 206 | try (InputStream template = getClass().getResourceAsStream("/plugin-settings.template.html")) { 207 | response.put("template", new String(template.readAllBytes(), StandardCharsets.UTF_8)); 208 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 209 | } 210 | } 211 | 212 | private GoPluginApiResponse handleValidatePluginSettingsConfiguration(GoPluginApiRequest goPluginApiRequest) { 213 | Map responseMap = (Map) JSONUtils.fromJSON(goPluginApiRequest.requestBody()); 214 | final Map configuration = keyValuePairs(responseMap, "plugin-settings"); 215 | List> response = new ArrayList<>(); 216 | 217 | validate(response, fieldValidation -> validateRequiredField(configuration, fieldValidation, PLUGIN_SETTINGS_SMTP_HOST, "SMTP Host")); 218 | validate(response, fieldValidation -> validateRequiredField(configuration, fieldValidation, PLUGIN_SETTINGS_SMTP_PORT, "SMTP Port")); 219 | validate(response, fieldValidation -> validateRequiredField(configuration, fieldValidation, PLUGIN_SETTINGS_IS_TLS, "TLS")); 220 | validate(response, fieldValidation -> validateRequiredField(configuration, fieldValidation, PLUGIN_SETTINGS_SENDER_EMAIL_ID, "Sender Email ID")); 221 | validate(response, fieldValidation -> validateRequiredField(configuration, fieldValidation, PLUGIN_SETTINGS_RECEIVER_EMAIL_ID, "Receiver Email-id")); 222 | 223 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 224 | } 225 | 226 | private void validate(List> response, FieldValidator fieldValidator) { 227 | Map fieldValidation = new HashMap<>(); 228 | fieldValidator.validate(fieldValidation); 229 | if (!fieldValidation.isEmpty()) { 230 | response.add(fieldValidation); 231 | } 232 | } 233 | 234 | private void validateRequiredField(Map configuration, Map fieldMap, String key, String name) { 235 | if (configuration.get(key) == null || configuration.get(key).isEmpty()) { 236 | fieldMap.put("key", key); 237 | fieldMap.put("message", String.format("'%s' is a required field", name)); 238 | } 239 | } 240 | 241 | public PluginSettings getPluginSettings() { 242 | Map requestMap = new HashMap<>(); 243 | requestMap.put("plugin-id", PLUGIN_ID); 244 | GoApiResponse response = goApplicationAccessor.submit(createGoApiRequest(GET_PLUGIN_SETTINGS, JSONUtils.toJSON(requestMap))); 245 | if (response.responseBody() == null || response.responseBody().trim().isEmpty()) { 246 | throw new RuntimeException("plugin is not configured. please provide plugin settings."); 247 | } 248 | Map responseBodyMap = (Map) JSONUtils.fromJSON(response.responseBody()); 249 | return new PluginSettings(responseBodyMap.get(PLUGIN_SETTINGS_SMTP_HOST), Integer.parseInt(responseBodyMap.get(PLUGIN_SETTINGS_SMTP_PORT)), 250 | Boolean.parseBoolean(responseBodyMap.get(PLUGIN_SETTINGS_IS_TLS)), responseBodyMap.get(PLUGIN_SETTINGS_SENDER_EMAIL_ID), 251 | responseBodyMap.get(PLUGIN_SETTINGS_SMTP_USERNAME), 252 | responseBodyMap.get(PLUGIN_SETTINGS_SENDER_PASSWORD), responseBodyMap.get(PLUGIN_SETTINGS_RECEIVER_EMAIL_ID), 253 | responseBodyMap.get(PLUGIN_SETTINGS_FILTER)); 254 | } 255 | 256 | private boolean isEmpty(String str) { 257 | return str == null || str.trim().isEmpty(); 258 | } 259 | 260 | private Map keyValuePairs(Map map, String mainKey) { 261 | Map keyValuePairs = new HashMap<>(); 262 | Map fieldsMap = (Map) map.get(mainKey); 263 | for (String field : fieldsMap.keySet()) { 264 | Map fieldProperties = (Map) fieldsMap.get(field); 265 | String value = (String) fieldProperties.get("value"); 266 | keyValuePairs.put(field, value); 267 | } 268 | return keyValuePairs; 269 | } 270 | 271 | private GoPluginIdentifier getGoPluginIdentifier() { 272 | return new GoPluginIdentifier(EXTENSION_NAME, goSupportedVersions); 273 | } 274 | 275 | private GoApiRequest createGoApiRequest(final String api, final String responseBody) { 276 | return new GoApiRequest() { 277 | @Override 278 | public String api() { 279 | return api; 280 | } 281 | 282 | @Override 283 | public String apiVersion() { 284 | return "1.0"; 285 | } 286 | 287 | @Override 288 | public GoPluginIdentifier pluginIdentifier() { 289 | return getGoPluginIdentifier(); 290 | } 291 | 292 | @Override 293 | public Map requestParameters() { 294 | return null; 295 | } 296 | 297 | @Override 298 | public Map requestHeaders() { 299 | return null; 300 | } 301 | 302 | @Override 303 | public String requestBody() { 304 | return responseBody; 305 | } 306 | }; 307 | } 308 | 309 | private GoPluginApiResponse renderJSON(final int responseCode, Object response) { 310 | final String json = response == null ? null : new GsonBuilder().create().toJson(response); 311 | return new GoPluginApiResponse() { 312 | @Override 313 | public int responseCode() { 314 | return responseCode; 315 | } 316 | 317 | @Override 318 | public Map responseHeaders() { 319 | return null; 320 | } 321 | 322 | @Override 323 | public String responseBody() { 324 | return json; 325 | } 326 | }; 327 | } 328 | 329 | void setSessionFactory(SessionFactory sessionFactory) { 330 | this.sessionFactory = sessionFactory; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/Filter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | 20 | import java.util.Objects; 21 | import java.util.regex.Pattern; 22 | import java.util.regex.PatternSyntaxException; 23 | 24 | public class Filter { 25 | 26 | private final String pipelinePattern; 27 | private final String stagePattern; 28 | private final BuildState status; 29 | 30 | public Filter(String pipelinePattern, String stagePattern, String status) { 31 | this.pipelinePattern = pipelinePattern; 32 | this.stagePattern = stagePattern; 33 | this.status = BuildState.fromRawString(status); 34 | } 35 | 36 | public boolean matches(String pipeline, String stage, BuildState status) { 37 | if (isNotAMatch(pipeline, convertToRegex(this.pipelinePattern))) { 38 | return false; 39 | } 40 | 41 | if (isNotAMatch(stage, convertToRegex(this.stagePattern))) { 42 | return false; 43 | } 44 | 45 | return this.status == null || this.status.equals(status); 46 | } 47 | 48 | private boolean isNotAMatch(String value, String pattern) { 49 | if (isARegex(pattern)) { 50 | if (!value.toLowerCase().matches(pattern.toLowerCase())) { 51 | return true; 52 | } 53 | } else if (pattern != null && !pattern.equalsIgnoreCase(value)) { 54 | return true; 55 | } 56 | return false; 57 | } 58 | 59 | private String convertToRegex(String pattern) { 60 | if (pattern == null || isARegex(pattern)) { 61 | return pattern; 62 | } 63 | return pattern.replaceAll("\\*", ".*"); 64 | } 65 | 66 | private boolean isARegex(String value) { 67 | try { 68 | if (value != null) { 69 | Pattern.compile(value); 70 | return true; 71 | } else { 72 | return false; 73 | } 74 | } catch (PatternSyntaxException e) { 75 | return false; 76 | } 77 | } 78 | 79 | public boolean matches(String pipeline, String stage, String rawStatus) { 80 | return matches(pipeline, stage, BuildState.fromRawString(rawStatus)); 81 | } 82 | 83 | @Override 84 | public int hashCode() { 85 | return Objects.hash(pipelinePattern, stagePattern, status); 86 | } 87 | 88 | @Override 89 | public boolean equals(Object o) { 90 | if (this == o) return true; 91 | if (o == null || getClass() != o.getClass()) return false; 92 | Filter filter = (Filter) o; 93 | return Objects.equals(pipelinePattern, filter.pipelinePattern) 94 | && Objects.equals(stagePattern, filter.stagePattern) 95 | && status == filter.status; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "Pipeline: " + pipelinePattern + "\n" + 101 | "Stage: " + stagePattern + "\n" + 102 | "Status: " + status + "\n"; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/FilterConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | public class FilterConverter { 23 | 24 | private static final String FILTER_SEPARATOR = ","; 25 | private static final String FILTER_FIELD_SEPARATOR = ":"; 26 | 27 | public List convertStringToFilterList(String filterConfigString) { 28 | ArrayList filterList = new ArrayList<>(); 29 | 30 | if(filterConfigString == null || "".equals(filterConfigString)) { 31 | return filterList; 32 | } 33 | 34 | String[] filterConfigList = convertToList(filterConfigString); 35 | 36 | for(String filterConfig : filterConfigList) { 37 | Filter converted = convertToFilter(filterConfig); 38 | if(converted != null) { 39 | filterList.add(converted); 40 | } 41 | } 42 | 43 | return filterList; 44 | } 45 | 46 | 47 | /** 48 | * Assumes a list of values separated by FILTER_SEPARATOR 49 | * 50 | * Each value will be treated as an individual filter 51 | * 52 | * E.g. 53 | * 54 | * "TempTest:cloudformation:building,SmokeTest:cloudformation:failed" 55 | * Would become 56 | * [ 57 | * "TempTest:cloudformation:building", 58 | * "SmokeTest:cloudformation:failed" 59 | * ] 60 | */ 61 | private String[] convertToList(String configList) { 62 | if(configList == null || "".equals(configList)) { 63 | return new String[0]; 64 | } 65 | 66 | String[] filterConfigList = new String[] { configList }; 67 | 68 | if(configList.contains(FILTER_SEPARATOR)) { 69 | filterConfigList = configList.split(FILTER_SEPARATOR); 70 | } 71 | 72 | return filterConfigList; 73 | } 74 | 75 | /** 76 | * Assumes each filter has at most 3 fields (pipeline/stage/status) split by FILTER_FIELD_SEPARATOR 77 | * 78 | * E.g. 79 | * 80 | * TempTestDeploy:cloudformation:building 81 | * 82 | * becomes a filter object with the 3 values set appropriately 83 | */ 84 | private Filter convertToFilter(String filterConfig) { 85 | if(filterConfig == null || "".equals(filterConfig)) { 86 | return null; 87 | } 88 | String pipeline = filterConfig; 89 | String stage = null; 90 | String status = null; 91 | 92 | if(filterConfig.contains(FILTER_FIELD_SEPARATOR)) { 93 | String[] filterConfigParts = filterConfig.split(FILTER_FIELD_SEPARATOR); 94 | 95 | pipeline = filterConfigParts[0]; 96 | 97 | if(filterConfigParts.length > 1) { 98 | stage = filterConfigParts[1]; 99 | } 100 | 101 | if(filterConfigParts.length > 2) { 102 | status = filterConfigParts[2]; 103 | } 104 | 105 | } 106 | 107 | return new Filter(pipeline, stage, status); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/PluginSettings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import java.util.List; 20 | 21 | public class PluginSettings { 22 | private String smtpHost; 23 | private int smtpPort; 24 | private boolean tls; 25 | private String senderEmailId; 26 | private String smtpUsername; 27 | private String senderPassword; 28 | private String receiverEmailId; 29 | private List filterList; 30 | 31 | public PluginSettings(String smtpHost, int smtpPort, boolean tls, String senderEmailId, String smtpUsername, String senderPassword, String receiverEmailId, String filterString) { 32 | this.smtpHost = smtpHost; 33 | this.smtpPort = smtpPort; 34 | this.tls = tls; 35 | this.senderEmailId = senderEmailId; 36 | this.smtpUsername = smtpUsername; 37 | this.senderPassword = senderPassword; 38 | this.receiverEmailId = receiverEmailId; 39 | FilterConverter filterController = new FilterConverter(); 40 | this.filterList = filterController.convertStringToFilterList(filterString); 41 | } 42 | 43 | public String getSmtpHost() { 44 | return smtpHost; 45 | } 46 | 47 | public void setSmtpHost(String smtpHost) { 48 | this.smtpHost = smtpHost; 49 | } 50 | 51 | public int getSmtpPort() { 52 | return smtpPort; 53 | } 54 | 55 | public void setSmtpPort(int smtpPort) { 56 | this.smtpPort = smtpPort; 57 | } 58 | 59 | public boolean isTls() { 60 | return tls; 61 | } 62 | 63 | public void setTls(boolean tls) { 64 | this.tls = tls; 65 | } 66 | 67 | public String getSenderEmailId() { 68 | return senderEmailId; 69 | } 70 | 71 | public void setSenderEmailId(String senderEmailId) { 72 | this.senderEmailId = senderEmailId; 73 | } 74 | 75 | public String getSenderPassword() { 76 | return senderPassword; 77 | } 78 | 79 | public void setSenderPassword(String senderPassword) { 80 | this.senderPassword = senderPassword; 81 | } 82 | 83 | public String getReceiverEmailId() { 84 | return receiverEmailId; 85 | } 86 | 87 | public void setReceiverEmailId(String receiverEmailId) { 88 | this.receiverEmailId = receiverEmailId; 89 | } 90 | 91 | public List getFilterList() { 92 | return filterList; 93 | } 94 | 95 | @Override 96 | public boolean equals(Object o) { 97 | if (this == o) return true; 98 | if (o == null || getClass() != o.getClass()) return false; 99 | 100 | PluginSettings that = (PluginSettings) o; 101 | 102 | if (smtpPort != that.smtpPort) return false; 103 | if (tls != that.tls) return false; 104 | if (receiverEmailId != null ? !receiverEmailId.equals(that.receiverEmailId) : that.receiverEmailId != null) 105 | return false; 106 | if (senderEmailId != null ? !senderEmailId.equals(that.senderEmailId) : that.senderEmailId != null) 107 | return false; 108 | if (senderPassword != null ? !senderPassword.equals(that.senderPassword) : that.senderPassword != null) 109 | return false; 110 | if (smtpHost != null ? !smtpHost.equals(that.smtpHost) : that.smtpHost != null) 111 | return false; 112 | if(filterList != null ? !filterList.equals(that.filterList) : that.filterList != null) 113 | return false; 114 | 115 | return true; 116 | } 117 | 118 | @Override 119 | public int hashCode() { 120 | int result = smtpHost != null ? smtpHost.hashCode() : 0; 121 | result = 31 * result + smtpPort; 122 | result = 31 * result + (tls ? 1 : 0); 123 | result = 31 * result + (senderEmailId != null ? senderEmailId.hashCode() : 0); 124 | result = 31 * result + (senderPassword != null ? senderPassword.hashCode() : 0); 125 | result = 31 * result + (receiverEmailId != null ? receiverEmailId.hashCode() : 0); 126 | result = 31 * result + (filterList != null ? filterList.hashCode() : 0); 127 | return result; 128 | } 129 | 130 | public String getSmtpUsername() { 131 | return smtpUsername; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/SMTPAuthenticator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import jakarta.mail.Authenticator; 20 | import jakarta.mail.PasswordAuthentication; 21 | 22 | final class SMTPAuthenticator extends Authenticator { 23 | private final String username; 24 | private final String password; 25 | 26 | public SMTPAuthenticator(String username, String password) { 27 | this.username = username; 28 | this.password = password; 29 | } 30 | 31 | protected PasswordAuthentication getPasswordAuthentication() { 32 | return new PasswordAuthentication(username, password); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/SMTPMailSender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import com.thoughtworks.go.plugin.api.logging.Logger; 20 | 21 | import jakarta.mail.*; 22 | import jakarta.mail.internet.MimeMessage; 23 | 24 | import java.util.Objects; 25 | import java.util.Properties; 26 | 27 | import static jakarta.mail.Message.RecipientType.TO; 28 | 29 | public class SMTPMailSender { 30 | private static final Logger LOGGER = Logger.getLoggerFor(EmailNotificationPluginImpl.class); 31 | 32 | private static final String FROM_PROPERTY = "mail.from"; 33 | private static final String TRANSPORT_PROTOCOL_PROPERTY = "mail.transport.protocol"; 34 | private static final String TIMEOUT_PROPERTY = "mail.smtp.timeout"; 35 | private static final String CONNECTION_TIMEOUT_PROPERTY = "mail.smtp.connectiontimeout"; 36 | private static final String STARTTLS_PROPERTY = "mail.smtp.starttls.enable"; 37 | private static final String TLS_CHECK_SERVER_IDENTITY_PROPERTY = "mail.smtp.ssl.checkserveridentity"; 38 | 39 | private static final int DEFAULT_MAIL_SENDER_TIMEOUT_IN_MILLIS = 60 * 1000; 40 | 41 | private final SMTPSettings smtpSettings; 42 | private final SessionFactory sessionFactory; 43 | 44 | public SMTPMailSender(SMTPSettings smtpSettings, SessionFactory sessionFactory) { 45 | this.smtpSettings = smtpSettings; 46 | this.sessionFactory = sessionFactory; 47 | } 48 | 49 | public void send(String subject, String body, String toEmailId) { 50 | Transport transport = null; 51 | try { 52 | Properties properties = mailProperties(); 53 | SessionWrapper sessionWrapper = createSession(properties, smtpSettings.getSmtpUsername(), smtpSettings.getPassword()); 54 | transport = sessionWrapper.getTransport(); 55 | transport.connect(smtpSettings.getHostName(), smtpSettings.getPort(), nullIfEmpty(smtpSettings.getSmtpUsername()), nullIfEmpty(smtpSettings.getPassword())); 56 | MimeMessage message = sessionWrapper.createMessage(smtpSettings.getFromEmailId(), toEmailId, subject, body); 57 | transport.sendMessage(message, message.getRecipients(TO)); 58 | } catch (Exception e) { 59 | LOGGER.error(String.format("Sending failed for email [%s] to [%s]", subject, toEmailId), e); 60 | } finally { 61 | if (transport != null) { 62 | try { 63 | transport.close(); 64 | } catch (MessagingException e) { 65 | LOGGER.error("Failed to close transport", e); 66 | } 67 | } 68 | } 69 | } 70 | 71 | private Properties mailProperties() { 72 | Properties props = new Properties(); 73 | props.put(FROM_PROPERTY, smtpSettings.getFromEmailId()); 74 | 75 | if (!System.getProperties().containsKey(CONNECTION_TIMEOUT_PROPERTY)) { 76 | props.put(CONNECTION_TIMEOUT_PROPERTY, DEFAULT_MAIL_SENDER_TIMEOUT_IN_MILLIS); 77 | } 78 | 79 | if (!System.getProperties().containsKey(TIMEOUT_PROPERTY)) { 80 | props.put(TIMEOUT_PROPERTY, DEFAULT_MAIL_SENDER_TIMEOUT_IN_MILLIS); 81 | } 82 | 83 | if (System.getProperties().containsKey(STARTTLS_PROPERTY)) { 84 | props.put(STARTTLS_PROPERTY, "true"); 85 | } 86 | 87 | if (!System.getProperties().containsKey(TLS_CHECK_SERVER_IDENTITY_PROPERTY)) { 88 | props.put(TLS_CHECK_SERVER_IDENTITY_PROPERTY, "true"); 89 | } 90 | 91 | props.put(TRANSPORT_PROTOCOL_PROPERTY, smtpSettings.isTls() ? "smtps" : "smtp"); 92 | 93 | return props; 94 | } 95 | 96 | private SessionWrapper createSession(Properties properties, String username, String password) { 97 | if (isEmpty(username) || isEmpty(password)) { 98 | return sessionFactory.getInstance(properties); 99 | } else { 100 | properties.put("mail.smtp.auth", "true"); 101 | properties.put("mail.smtps.auth", "true"); 102 | return sessionFactory.getInstance(properties, new SMTPAuthenticator(username, password)); 103 | } 104 | } 105 | 106 | private String nullIfEmpty(String str) { 107 | return isEmpty(str) ? null : str; 108 | } 109 | 110 | private boolean isEmpty(String str) { 111 | return str == null || str.trim().isEmpty(); 112 | } 113 | 114 | @Override 115 | public boolean equals(Object o) { 116 | if (this == o) return true; 117 | if (o == null || getClass() != o.getClass()) return false; 118 | 119 | SMTPMailSender that = (SMTPMailSender) o; 120 | 121 | return Objects.equals(smtpSettings, that.smtpSettings); 122 | } 123 | 124 | @Override 125 | public int hashCode() { 126 | return smtpSettings != null ? smtpSettings.hashCode() : 0; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/SMTPSettings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | public class SMTPSettings { 20 | private String hostName; 21 | private int port; 22 | private boolean tls; 23 | private String fromEmailId; 24 | private String password; 25 | private String smtpUsername; 26 | 27 | public SMTPSettings(String hostName, int port, boolean tls, String fromEmailId, String smtpUsername, String password) { 28 | this.hostName = hostName; 29 | this.port = port; 30 | this.tls = tls; 31 | this.password = password; 32 | this.smtpUsername = smtpUsername; 33 | this.fromEmailId = fromEmailId; 34 | } 35 | 36 | public String getHostName() { 37 | return hostName; 38 | } 39 | 40 | public int getPort() { 41 | return port; 42 | } 43 | 44 | public boolean isTls() { 45 | return tls; 46 | } 47 | 48 | public String getFromEmailId() { 49 | return fromEmailId; 50 | } 51 | 52 | public String getPassword() { 53 | return password; 54 | } 55 | 56 | public String getSmtpUsername() { return smtpUsername; } 57 | 58 | @Override 59 | public boolean equals(Object o) { 60 | if (this == o) return true; 61 | if (o == null || getClass() != o.getClass()) return false; 62 | 63 | SMTPSettings that = (SMTPSettings) o; 64 | 65 | if (port != that.port) return false; 66 | if (tls != that.tls) return false; 67 | if (fromEmailId != null ? !fromEmailId.equals(that.fromEmailId) : that.fromEmailId != null) return false; 68 | if (hostName != null ? !hostName.equals(that.hostName) : that.hostName != null) return false; 69 | if (password != null ? !password.equals(that.password) : that.password != null) return false; 70 | if (smtpUsername != null ? !smtpUsername.equals(that.smtpUsername) : that.smtpUsername != null) return false; 71 | 72 | return true; 73 | } 74 | 75 | @Override 76 | public int hashCode() { 77 | int result = hostName != null ? hostName.hashCode() : 0; 78 | result = 31 * result + port; 79 | result = 31 * result + (tls ? 1 : 0); 80 | result = 31 * result + (fromEmailId != null ? fromEmailId.hashCode() : 0); 81 | result = 31 * result + (password != null ? password.hashCode() : 0); 82 | result = 31 * result + (smtpUsername != null ? smtpUsername.hashCode() : 0); 83 | return result; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/SessionFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import jakarta.mail.Authenticator; 20 | import jakarta.mail.Session; 21 | import java.util.Properties; 22 | 23 | public class SessionFactory { 24 | public SessionFactory() { 25 | } 26 | 27 | public SessionWrapper getInstance(Properties properties) { 28 | return new SessionWrapper(Session.getInstance(properties)); 29 | } 30 | 31 | public SessionWrapper getInstance(Properties properties, Authenticator smtpAuthenticator) { 32 | return new SessionWrapper(Session.getInstance(properties, smtpAuthenticator)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/SessionWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import jakarta.mail.MessagingException; 20 | import jakarta.mail.NoSuchProviderException; 21 | import jakarta.mail.Session; 22 | import jakarta.mail.Transport; 23 | import jakarta.mail.internet.InternetAddress; 24 | import jakarta.mail.internet.MimeMessage; 25 | import java.util.Date; 26 | 27 | import static jakarta.mail.Message.RecipientType.TO; 28 | 29 | public class SessionWrapper { 30 | private final Session instance; 31 | 32 | public SessionWrapper(Session instance) { 33 | this.instance = instance; 34 | } 35 | 36 | public Transport getTransport() throws NoSuchProviderException { 37 | return instance.getTransport(); 38 | } 39 | 40 | public MimeMessage createMessage(String fromEmailId, String toEmailId, String subject, String body) throws MessagingException { 41 | MimeMessage message = new MimeMessage(instance); 42 | message.setFrom(new InternetAddress(fromEmailId)); 43 | message.setRecipients(TO, toEmailId); 44 | message.setSubject(subject); 45 | message.setContent(message, "text/plain"); 46 | message.setSentDate(new Date()); 47 | message.setText(body); 48 | message.setSender(new InternetAddress(fromEmailId)); 49 | message.setReplyTo(new InternetAddress[]{new InternetAddress(fromEmailId)}); 50 | return message; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/util/FieldValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin.util; 18 | 19 | import java.util.Map; 20 | 21 | public interface FieldValidator { 22 | public void validate(Map fieldValidation); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/tw/go/plugin/util/JSONUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin.util; 18 | 19 | import com.google.gson.GsonBuilder; 20 | 21 | public class JSONUtils { 22 | public static Object fromJSON(String json) { 23 | return new GsonBuilder().create().fromJson(json, Object.class); 24 | } 25 | 26 | public static String toJSON(Object object) { 27 | return new GsonBuilder().create().toJson(object); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/plugin-settings.template.html: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 19 | 20 | {{ GOINPUTNAME[smtp_host].$error.server }} 21 |
22 |
23 | 24 | 25 | {{ GOINPUTNAME[smtp_port].$error.server }} 26 |
27 |
28 | 29 | True 30 | False 31 | {{ GOINPUTNAME[is_tls].$error.server }} 32 |
33 |
34 | 35 | 36 | {{ GOINPUTNAME[sender_email_id].$error.server }} 37 |
38 |
39 | 40 | 41 | {{ GOINPUTNAME[smtp_username].$error.server }} 42 |
43 |
44 | 45 | 46 | {{ GOINPUTNAME[sender_password].$error.server }} 47 |
48 |
49 | 50 | 51 | {{ GOINPUTNAME[receiver_email_id].$error.server }} 52 |
53 |
54 | 55 | 56 | {{ GOINPUTNAME[pipeline_stage_filter].$error.server }} 57 |
-------------------------------------------------------------------------------- /src/test/java/com/tw/go/plugin/EmailNotificationPluginImplUnitTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import com.thoughtworks.go.plugin.api.GoApplicationAccessor; 20 | import com.thoughtworks.go.plugin.api.GoPluginIdentifier; 21 | import com.thoughtworks.go.plugin.api.request.GoApiRequest; 22 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; 23 | import com.thoughtworks.go.plugin.api.response.GoApiResponse; 24 | import com.tw.go.plugin.util.JSONUtils; 25 | import org.junit.jupiter.api.BeforeEach; 26 | import org.junit.jupiter.api.Test; 27 | import org.mockito.ArgumentCaptor; 28 | import org.mockito.Mock; 29 | import org.mockito.MockitoAnnotations; 30 | 31 | import jakarta.mail.Address; 32 | import jakarta.mail.Authenticator; 33 | import jakarta.mail.Message; 34 | import jakarta.mail.Transport; 35 | import jakarta.mail.internet.InternetAddress; 36 | import java.util.Collections; 37 | import java.util.HashMap; 38 | import java.util.Map; 39 | import java.util.Properties; 40 | 41 | import static org.junit.jupiter.api.Assertions.assertEquals; 42 | import static org.junit.jupiter.api.Assertions.assertNotNull; 43 | import static org.mockito.Mockito.*; 44 | 45 | public class EmailNotificationPluginImplUnitTest { 46 | 47 | @Mock 48 | private GoApplicationAccessor goApplicationAccessor; 49 | 50 | @Mock 51 | private SessionWrapper mockSession; 52 | 53 | @Mock 54 | private Transport mockTransport; 55 | 56 | @Mock 57 | private SessionFactory mockSessionFactory; 58 | 59 | private Map settingsResponseMap; 60 | 61 | private Map stateChangeResponseMap; 62 | private EmailNotificationPluginImpl emailNotificationPlugin; 63 | 64 | 65 | @BeforeEach 66 | public void setup() throws Exception { 67 | MockitoAnnotations.openMocks(this); 68 | 69 | when(mockSession.getTransport()).thenReturn(mockTransport); 70 | 71 | when(mockSessionFactory.getInstance(any(Properties.class))).thenReturn(mockSession); 72 | when(mockSessionFactory.getInstance(any(Properties.class), any(Authenticator.class))).thenReturn(mockSession); 73 | 74 | emailNotificationPlugin = new EmailNotificationPluginImpl(); 75 | emailNotificationPlugin.initializeGoApplicationAccessor(goApplicationAccessor); 76 | emailNotificationPlugin.setSessionFactory(mockSessionFactory); 77 | } 78 | 79 | @BeforeEach 80 | public void setupDefaultSettingResponse() { 81 | settingsResponseMap = new HashMap<>(); 82 | 83 | settingsResponseMap.put("smtp_host", "test-smtp-host"); 84 | settingsResponseMap.put("smtp_port", "25"); 85 | settingsResponseMap.put("is_tls", "0"); 86 | settingsResponseMap.put("sender_email_id", "test-smtp-sender"); 87 | settingsResponseMap.put("sender_password", "test-smtp-password"); 88 | settingsResponseMap.put("smtp_username", "test-smtp-username"); 89 | settingsResponseMap.put("receiver_email_id", "test-smtp-receiver"); 90 | } 91 | 92 | @BeforeEach 93 | public void setupDefaultStateChangeResponseMap() { 94 | Map stageResponseMap = new HashMap<>(); 95 | 96 | stageResponseMap.put("name", "test-stage-name"); 97 | stageResponseMap.put("counter", "test-counter"); 98 | stageResponseMap.put("state", "test-state"); 99 | stageResponseMap.put("result", "test-result"); 100 | stageResponseMap.put("last-transition-time", "test-last-transition-time"); 101 | stageResponseMap.put("create-time", "test-last-transition-time"); 102 | 103 | 104 | Map pipelineMap = new HashMap<>(); 105 | 106 | pipelineMap.put("stage", stageResponseMap); 107 | pipelineMap.put("name", "test-pipeline-name"); 108 | pipelineMap.put("counter", "test-pipeline-counter"); 109 | 110 | stateChangeResponseMap = new HashMap<>(); 111 | 112 | stateChangeResponseMap.put("pipeline", pipelineMap); 113 | } 114 | 115 | 116 | @Test 117 | public void testStageNotificationRequestsSettings() { 118 | GoApiResponse settingsResponse = testSettingsResponse(); 119 | 120 | when(goApplicationAccessor.submit(eq(testSettingsRequest()))).thenReturn(settingsResponse); 121 | 122 | GoPluginApiRequest requestFromServer = testStageChangeRequestFromServer(); 123 | 124 | emailNotificationPlugin.handle(requestFromServer); 125 | 126 | final ArgumentCaptor settingsRequestCaptor = ArgumentCaptor.forClass(GoApiRequest.class); 127 | 128 | verify(goApplicationAccessor).submit(settingsRequestCaptor.capture()); 129 | 130 | final GoApiRequest actualSettingsRequest = settingsRequestCaptor.getValue(); 131 | 132 | assertEquals(testSettingsRequest().api(), actualSettingsRequest.api()); 133 | assertEquals(testSettingsRequest().apiVersion(), actualSettingsRequest.apiVersion()); 134 | 135 | GoPluginIdentifier actualGoPluginIdentifier = actualSettingsRequest.pluginIdentifier(); 136 | 137 | assertNotNull(actualGoPluginIdentifier); 138 | 139 | assertEquals(testSettingsRequest().pluginIdentifier().getExtension(), actualGoPluginIdentifier.getExtension()); 140 | assertEquals(testSettingsRequest().pluginIdentifier().getSupportedExtensionVersions(), actualGoPluginIdentifier.getSupportedExtensionVersions()); 141 | assertEquals(testSettingsRequest().requestBody(), actualSettingsRequest.requestBody()); 142 | assertEquals(testSettingsRequest().requestHeaders(), actualSettingsRequest.requestHeaders()); 143 | assertEquals(testSettingsRequest().requestParameters(), actualSettingsRequest.requestParameters()); 144 | } 145 | 146 | @Test 147 | public void testASingleEmailAddressSendsEmail() throws Exception { 148 | settingsResponseMap.put("receiver_email_id", "test-email@test.co.uk"); 149 | 150 | GoApiResponse settingsResponse = testSettingsResponse(); 151 | 152 | when(goApplicationAccessor.submit(any(GoApiRequest.class))).thenReturn(settingsResponse); 153 | doCallRealMethod().when(mockSession).createMessage(anyString(), anyString(), anyString(), anyString()); 154 | 155 | GoPluginApiRequest requestFromServer = testStageChangeRequestFromServer(); 156 | 157 | emailNotificationPlugin.handle(requestFromServer); 158 | 159 | verify(mockTransport).sendMessage(any(Message.class), eq(new Address[]{new InternetAddress("test-email@test.co.uk")})); 160 | verify(mockTransport, times(1)).connect(eq("test-smtp-host"), eq(25), eq("test-smtp-username"), eq("test-smtp-password")); 161 | verify(mockTransport, times(1)).close(); 162 | verifyNoMoreInteractions(mockTransport); 163 | } 164 | 165 | @Test 166 | public void testMultipleEmailAddressSendsEmail() throws Exception { 167 | settingsResponseMap.put("receiver_email_id", "test-email@test.co.uk, test-email-2@test.co.uk"); 168 | 169 | GoApiResponse settingsResponse = testSettingsResponse(); 170 | 171 | when(goApplicationAccessor.submit(any(GoApiRequest.class))).thenReturn(settingsResponse); 172 | doCallRealMethod().when(mockSession).createMessage(anyString(), anyString(), anyString(), anyString()); 173 | 174 | GoPluginApiRequest requestFromServer = testStageChangeRequestFromServer(); 175 | 176 | emailNotificationPlugin.handle(requestFromServer); 177 | 178 | verify(mockTransport).sendMessage(any(Message.class), eq(new Address[]{new InternetAddress("test-email@test.co.uk")})); 179 | verify(mockTransport).sendMessage(any(Message.class), eq(new Address[]{new InternetAddress("test-email-2@test.co.uk")})); 180 | verify(mockTransport, times(2)).connect(eq("test-smtp-host"), eq(25), eq("test-smtp-username"), eq("test-smtp-password")); 181 | verify(mockTransport, times(2)).close(); 182 | verifyNoMoreInteractions(mockTransport); 183 | } 184 | 185 | 186 | private GoPluginApiRequest testStageChangeRequestFromServer() { 187 | GoPluginApiRequest requestFromGoServer = mock(GoPluginApiRequest.class); 188 | 189 | when(requestFromGoServer.requestName()).thenReturn("stage-status"); 190 | 191 | when(requestFromGoServer.requestBody()).thenReturn(JSONUtils.toJSON(stateChangeResponseMap)); 192 | 193 | return requestFromGoServer; 194 | } 195 | 196 | private static GoApiRequest testSettingsRequest() { 197 | 198 | final Map requestMap = new HashMap<>(); 199 | requestMap.put("plugin-id", "email.notifier"); 200 | 201 | final String responseBody = JSONUtils.toJSON(requestMap); 202 | 203 | return new GoApiRequest() { 204 | @Override 205 | public String api() { 206 | return "go.processor.plugin-settings.get"; 207 | } 208 | 209 | @Override 210 | public String apiVersion() { 211 | return "1.0"; 212 | } 213 | 214 | @Override 215 | public GoPluginIdentifier pluginIdentifier() { 216 | return new GoPluginIdentifier("notification", Collections.singletonList("1.0")); 217 | } 218 | 219 | @Override 220 | public Map requestParameters() { 221 | return null; 222 | } 223 | 224 | @Override 225 | public Map requestHeaders() { 226 | return null; 227 | } 228 | 229 | @Override 230 | public String requestBody() { 231 | return responseBody; 232 | } 233 | }; 234 | } 235 | 236 | private GoApiResponse testSettingsResponse() { 237 | GoApiResponse settingsResponse = mock(GoApiResponse.class); 238 | 239 | when(settingsResponse.responseBody()).thenReturn(JSONUtils.toJSON(settingsResponseMap)); 240 | 241 | return settingsResponse; 242 | } 243 | 244 | 245 | } -------------------------------------------------------------------------------- /src/test/java/com/tw/go/plugin/FilterConverterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.List; 23 | 24 | import static org.junit.jupiter.api.Assertions.assertEquals; 25 | import static org.junit.jupiter.api.Assertions.assertNotNull; 26 | 27 | public class FilterConverterTest { 28 | 29 | private FilterConverter filterConverter; 30 | 31 | @BeforeEach 32 | public void setup() { 33 | this.filterConverter = new FilterConverter(); 34 | } 35 | 36 | @Test 37 | public void testSingleFilterConverted() { 38 | String settingInput = "TempTestBaseInf:cloudformation:building"; 39 | 40 | List filterList = filterConverter.convertStringToFilterList(settingInput); 41 | 42 | assertNotNull(filterList); 43 | assertEquals(1, filterList.size()); 44 | 45 | Filter expectedFilter = new Filter("TempTestBaseInf", "cloudformation", "building"); 46 | 47 | assertEquals(expectedFilter, filterList.get(0)); 48 | } 49 | 50 | @Test 51 | public void testMultipleFiltersConverted() { 52 | String settingInput = "TempTestBaseInf:cloudformation:building,SmokeTestNetworkInf:ansible:failed"; 53 | 54 | List filterList = filterConverter.convertStringToFilterList(settingInput); 55 | 56 | assertNotNull(filterList); 57 | assertEquals(2, filterList.size()); 58 | 59 | Filter firstExpectedFilter = new Filter("TempTestBaseInf", "cloudformation", "building"); 60 | Filter secondExpectedFilter = new Filter("SmokeTestNetworkInf", "ansible", "failed"); 61 | 62 | assertEquals(firstExpectedFilter, filterList.get(0)); 63 | assertEquals(secondExpectedFilter, filterList.get(1)); 64 | } 65 | 66 | @Test 67 | public void testMultipleFiltersConvertedWithExtraCommas() { 68 | String settingInput = ",,TempTestBaseInf:cloudformation:building,,,,SmokeTestNetworkInf:ansible:failed,,,"; 69 | 70 | List filterList = filterConverter.convertStringToFilterList(settingInput); 71 | 72 | assertNotNull(filterList); 73 | assertEquals(2, filterList.size()); 74 | 75 | Filter firstExpectedFilter = new Filter("TempTestBaseInf", "cloudformation", "building"); 76 | Filter secondExpectedFilter = new Filter("SmokeTestNetworkInf", "ansible", "failed"); 77 | 78 | assertEquals(firstExpectedFilter, filterList.get(0)); 79 | assertEquals(secondExpectedFilter, filterList.get(1)); 80 | } 81 | 82 | @Test 83 | public void testPartiallyDefinedFilter() { 84 | String settingInput = "TempTestBaseInf"; 85 | 86 | List filterList = filterConverter.convertStringToFilterList(settingInput); 87 | 88 | assertNotNull(filterList); 89 | assertEquals(1, filterList.size()); 90 | 91 | Filter expectedFilter = new Filter("TempTestBaseInf", null, null); 92 | 93 | assertEquals(expectedFilter, filterList.get(0)); 94 | } 95 | 96 | @Test 97 | public void testMultiplePartiallyDefinedFilters() { 98 | String settingInput = "TempTestBaseInf,SmokeTest:cloudformation"; 99 | 100 | List filterList = filterConverter.convertStringToFilterList(settingInput); 101 | 102 | assertNotNull(filterList); 103 | assertEquals(2, filterList.size()); 104 | 105 | Filter firstExpectedFilter = new Filter("TempTestBaseInf", null, null); 106 | Filter secondExpectedFilter = new Filter("SmokeTest", "cloudformation", null); 107 | 108 | assertEquals(firstExpectedFilter, filterList.get(0)); 109 | assertEquals(secondExpectedFilter, filterList.get(1)); 110 | } 111 | 112 | @Test 113 | public void testConvertMultipleFiltersWithWildcards() { 114 | String settingInput = "*BaseInf*:cloudformation:failed,*NetworkInf*:cloudformation:building"; 115 | 116 | List filterList = filterConverter.convertStringToFilterList(settingInput); 117 | 118 | assertNotNull(filterList); 119 | assertEquals(2, filterList.size()); 120 | 121 | Filter firstExpectedFilter = new Filter("*BaseInf*", "cloudformation", "failed"); 122 | Filter secondExpectedFilter = new Filter("*NetworkInf*", "cloudformation", "building"); 123 | 124 | assertEquals(firstExpectedFilter, filterList.get(0)); 125 | assertEquals(secondExpectedFilter, filterList.get(1)); 126 | } 127 | } -------------------------------------------------------------------------------- /src/test/java/com/tw/go/plugin/FilterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertFalse; 22 | import static org.junit.jupiter.api.Assertions.assertTrue; 23 | 24 | public class FilterTest { 25 | 26 | @Test 27 | public void testSimpleFilterIsMatched() { 28 | Filter testFilter = new Filter("SmokeTestBaseInf", "cloudformation", "building"); 29 | 30 | assertTrue(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.BUILDING)); 31 | assertTrue(testFilter.matches("SmokeTestBaseInf", "cloudformation", "building")); 32 | 33 | assertFalse(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.CANCELLED)); 34 | assertFalse(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.FAILED)); 35 | assertFalse(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.FAILING)); 36 | assertFalse(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.PASSED)); 37 | assertFalse(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.UNKNOWN)); 38 | } 39 | 40 | @Test 41 | public void testFilterWithNullBuildStateMatchesAllStates() { 42 | Filter testFilter = new Filter("SmokeTestBaseInf", "cloudformation", null); 43 | 44 | for(BuildState currentState : BuildState.values()) { 45 | assertTrue(testFilter.matches("SmokeTestBaseInf", "cloudformation", currentState)); 46 | assertTrue(testFilter.matches("SmokeTestBaseInf", "ClOuDfOrMaTiOn", currentState)); 47 | } 48 | } 49 | 50 | @Test 51 | public void testFilterWithNullStageNameAndBuildStateMatchesAllStates() { 52 | Filter testFilter = new Filter("SmokeTestBaseInf", null, null); 53 | 54 | for(BuildState currentState : BuildState.values()) { 55 | assertTrue(testFilter.matches("SmokeTestBaseInf", null, currentState)); 56 | } 57 | } 58 | 59 | @Test 60 | public void testFilterWithNullStageNameAndBuildStateMatchesAllStageNames() { 61 | Filter testFilter = new Filter("SmokeTestBaseInf", null, null); 62 | 63 | assertTrue(testFilter.matches("SmokeTestBaseInf", null, BuildState.BUILDING)); 64 | assertTrue(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.BUILDING)); 65 | assertTrue(testFilter.matches("SmokeTestBaseInf", "CLOUDforMATION", BuildState.BUILDING)); 66 | assertTrue(testFilter.matches("SmokeTestBaseInf", "somethingrandom", BuildState.BUILDING)); 67 | } 68 | 69 | @Test 70 | public void testFilterWithWilcardPipelineNameMatchesAllPipelines() { 71 | Filter testFilter = new Filter("*BaseInf*", "cloudformation", "building"); 72 | 73 | assertTrue(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.BUILDING)); 74 | assertTrue(testFilter.matches("TempTestBaseInf", "cloudformation", BuildState.BUILDING)); 75 | assertTrue(testFilter.matches("ProductionBaseInf", "cloudformation", BuildState.BUILDING)); 76 | } 77 | 78 | @Test 79 | public void testFilterWithRegexPipelineNameMatchesPipelines() { 80 | Filter testFilter = new Filter(".*BaseInf", "cloudformation", "building"); 81 | 82 | assertTrue(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.BUILDING)); 83 | assertTrue(testFilter.matches("TempTestBaseInf", "cloudformation", BuildState.BUILDING)); 84 | assertTrue(testFilter.matches("ProductionBaseInf", "cloudformation", BuildState.BUILDING)); 85 | } 86 | 87 | @Test 88 | public void testFilterWithRegexStageNameMatchesStages() { 89 | Filter testFilter = new Filter("SmokeTestBaseInf", ".*formation", "building"); 90 | 91 | assertTrue(testFilter.matches("SmokeTestBaseInf", "bananaformation", BuildState.BUILDING)); 92 | assertTrue(testFilter.matches("SmokeTestBaseInf", "pickleformation", BuildState.BUILDING)); 93 | assertTrue(testFilter.matches("SmokeTestBaseInf", "cloudformation", BuildState.BUILDING)); 94 | } 95 | 96 | @Test 97 | public void testFilterWithRegexStageAndPipelineNameMatchesStages() { 98 | Filter testFilter = new Filter(".*BaseInf", ".*formation", "building"); 99 | 100 | assertTrue(testFilter.matches("SmokeTestBaseInf", "bananaformation", BuildState.BUILDING)); 101 | assertTrue(testFilter.matches("TempTestBaseInf", "pickleformation", BuildState.BUILDING)); 102 | assertTrue(testFilter.matches("ProductionBaseInf", "cloudformation", BuildState.BUILDING)); 103 | } 104 | } -------------------------------------------------------------------------------- /src/test/java/com/tw/go/plugin/SMTPMailSenderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tw.go.plugin; 18 | 19 | import com.icegreen.greenmail.util.GreenMail; 20 | import com.icegreen.greenmail.util.ServerSetupTest; 21 | import org.junit.jupiter.api.AfterEach; 22 | import org.junit.jupiter.api.BeforeEach; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import jakarta.mail.MessagingException; 26 | import java.io.IOException; 27 | 28 | import static org.hamcrest.CoreMatchers.is; 29 | import static org.hamcrest.MatcherAssert.assertThat; 30 | 31 | public class SMTPMailSenderTest { 32 | private GreenMail mailServer; 33 | 34 | @BeforeEach 35 | public void setUp() { 36 | mailServer = new GreenMail(ServerSetupTest.SMTP); 37 | mailServer.start(); 38 | } 39 | 40 | @Test 41 | public void shouldSendEmail() throws MessagingException, IOException { 42 | String userName = "user1"; 43 | String emailId = "user1@domain.com"; 44 | String password = "password1"; 45 | 46 | mailServer.setUser(emailId, userName, password); 47 | SMTPSettings settings = new SMTPSettings("127.0.0.1", ServerSetupTest.SMTP.getPort(), false, emailId, userName, password); 48 | new SMTPMailSender(settings, new SessionFactory()).send("subject", "body", emailId); 49 | 50 | assertThat(mailServer.getReceivedMessages().length, is(1)); 51 | assertThat(mailServer.getReceivedMessages()[0].getFrom()[0].toString(), is(emailId)); 52 | assertThat(mailServer.getReceivedMessages()[0].getSubject(), is("subject")); 53 | assertThat(mailServer.getReceivedMessages()[0].getContent().toString(), is("body")); 54 | } 55 | 56 | @AfterEach 57 | public void tearDown() { 58 | mailServer.stop(); 59 | } 60 | } --------------------------------------------------------------------------------