├── .github
├── release-drafter.yml
├── renovate.json
└── workflows
│ ├── gradle.yml
│ ├── release-notes.yml
│ └── release.yml
├── .gitignore
├── .sdkmanrc
├── LICENSE
├── README.md
├── build.gradle
├── buildSrc
└── build.gradle
├── examples
└── testapp1
│ ├── .gitignore
│ ├── .sdkmanrc
│ ├── build.gradle
│ ├── buildSrc
│ └── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── grails-app
│ ├── assets
│ │ ├── images
│ │ │ ├── advancedgrails.svg
│ │ │ ├── apple-touch-icon-retina.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── documentation.svg
│ │ │ ├── favicon.ico
│ │ │ ├── grails-cupsonly-logo-white.svg
│ │ │ ├── grails.svg
│ │ │ └── slack.svg
│ │ ├── javascripts
│ │ │ └── application.js
│ │ └── stylesheets
│ │ │ ├── application.css
│ │ │ ├── errors.css
│ │ │ └── grails.css
│ ├── conf
│ │ ├── application.yml
│ │ └── logback-spring.xml
│ ├── controllers
│ │ └── testapp1
│ │ │ ├── GreenMailController.groovy
│ │ │ ├── SendMailController.groovy
│ │ │ └── UrlMappings.groovy
│ ├── i18n
│ │ ├── messages.properties
│ │ └── messages_fr.properties
│ ├── init
│ │ └── testapp1
│ │ │ ├── Application.groovy
│ │ │ └── BootStrap.groovy
│ ├── services
│ │ └── testapp1
│ │ │ └── GreenMailService.groovy
│ └── views
│ │ ├── _testemails
│ │ ├── i18ntest.gsp
│ │ ├── newLineTagTest.gsp
│ │ ├── newLineTest.gsp
│ │ ├── tagtest.gsp
│ │ ├── test.gsp
│ │ └── testhtml.gsp
│ │ ├── error.gsp
│ │ ├── greenMail
│ │ └── greenMail.gsp
│ │ ├── index.gsp
│ │ ├── layouts
│ │ └── main.gsp
│ │ └── notFound.gsp
│ ├── grails-wrapper.jar
│ ├── grailsw
│ ├── grailsw.bat
│ ├── settings.gradle
│ └── src
│ └── integration-test
│ ├── groovy
│ └── grails
│ │ └── plugins
│ │ └── mail
│ │ ├── MailServiceSpec.groovy
│ │ └── functional
│ │ └── MailSendSpec.groovy
│ └── resources
│ ├── assets
│ └── grailslogo.png
│ └── logback-test.xml
├── gradle.properties
├── gradle
├── docs-config.gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── docs
└── index.adoc
├── main
├── groovy
│ └── grails
│ │ └── plugins
│ │ └── mail
│ │ ├── GrailsMailException.groovy
│ │ ├── MailAutoConfiguration.groovy
│ │ ├── MailConfigurationProperties.groovy
│ │ ├── MailGrailsPlugin.groovy
│ │ ├── MailMessageBuilder.groovy
│ │ ├── MailMessageBuilderFactory.groovy
│ │ ├── MailMessageContentRender.groovy
│ │ ├── MailMessageContentRenderer.groovy
│ │ ├── MailService.groovy
│ │ ├── PlainTextMailTagLib.groovy
│ │ └── SendMail.groovy
└── resources
│ └── META-INF
│ └── spring
│ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── test
└── groovy
└── grails
└── plugins
└── mail
└── MailMessageBuilderSpec.groovy
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: $RESOLVED_VERSION
2 | tag-template: v$RESOLVED_VERSION
3 | pull-request:
4 | title-templates:
5 | fix: '🐛 $TITLE (#$NUMBER)'
6 | feat: '🚀 $TITLE (#$NUMBER)'
7 | default: '$TITLE (#$NUMBER)'
8 | autolabeler:
9 | - label: 'bug'
10 | branch:
11 | - '/fix\/.+/'
12 | title:
13 | - '/fix/i'
14 | - label: 'improvement'
15 | branch:
16 | - '/improv\/.+/'
17 | title:
18 | - '/improv/i'
19 | - label: 'feature'
20 | branch:
21 | - '/feature\/.+/'
22 | title:
23 | - '/feat/i'
24 | - label: 'documentation'
25 | branch:
26 | - '/docs\/.+/'
27 | title:
28 | - '/docs/i'
29 | - label: 'maintenance'
30 | branch:
31 | - '/(chore|refactor|style|test|ci|perf|build)\/.+/'
32 | title:
33 | - '/(chore|refactor|style|test|ci|perf|build)/i'
34 | - label: 'chore'
35 | branch:
36 | - '/chore\/.+/'
37 | title:
38 | - '/chore/i'
39 | - label: 'refactor'
40 | branch:
41 | - '/refactor\/.+/'
42 | title:
43 | - '/refactor/i'
44 | - label: 'style'
45 | branch:
46 | - '/style\/.+/'
47 | title:
48 | - '/style/i'
49 | - label: 'test'
50 | branch:
51 | - '/test\/.+/'
52 | title:
53 | - '/test/i'
54 | - label: 'ci'
55 | branch:
56 | - '/ci\/.+/'
57 | title:
58 | - '/ci/i'
59 | - label: 'perf'
60 | branch:
61 | - '/perf\/.+/'
62 | title:
63 | - '/perf/i'
64 | - label: 'build'
65 | branch:
66 | - '/build\/.+/'
67 | title:
68 | - '/build/i'
69 | - label: 'deps'
70 | branch:
71 | - '/deps\/.+/'
72 | title:
73 | - '/deps/i'
74 | - label: 'revert'
75 | branch:
76 | - '/revert\/.+/'
77 | title:
78 | - '/revert/i'
79 | categories:
80 | - title: '🚀 Features'
81 | labels:
82 | - 'feature'
83 | - "type: enhancement"
84 | - "type: new feature"
85 | - "type: major"
86 | - "type: minor"
87 | - title: '💡 Improvements'
88 | labels:
89 | - 'improvement'
90 | - "type: improvement"
91 |
92 | - title: '🐛 Bug Fixes'
93 | labels:
94 | - 'fix'
95 | - 'bug'
96 | - "type: bug"
97 | - title: '📚 Documentation'
98 | labels:
99 | - 'docs'
100 | - title: '🔧 Maintenance'
101 | labels:
102 | - 'maintenance'
103 | - 'chore'
104 | - 'refactor'
105 | - 'style'
106 | - 'test'
107 | - 'ci'
108 | - 'perf'
109 | - 'build'
110 | - "type: ci"
111 | - "type: build"
112 | - title: '⏪ Reverts'
113 | labels:
114 | - 'revert'
115 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
116 | version-resolver:
117 | major:
118 | labels:
119 | - 'type: major'
120 | minor:
121 | labels:
122 | - 'type: minor'
123 | patch:
124 | labels:
125 | - 'type: patch'
126 | default: patch
127 | template: |
128 | ## What's Changed
129 |
130 | $CHANGES
131 |
132 | ## Contributors
133 |
134 | $CONTRIBUTORS
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "labels": ["type: dependency upgrade"],
6 | "packageRules": [
7 | {
8 | "matchUpdateTypes": ["major"],
9 | "enabled": false
10 | },
11 | {
12 | "matchPackagePatterns": ["*"],
13 | "allowedVersions": "!/SNAPSHOT$/"
14 | },
15 | {
16 | "matchPackagePatterns": [
17 | "^org\\.codehaus\\.groovy"
18 | ],
19 | "groupName": "groovy monorepo"
20 | },
21 | {
22 | "matchPackageNames": [
23 | "org.grails:grails-bom",
24 | "org.grails:grails-bootstrap",
25 | "org.grails:grails-codecs",
26 | "org.grails:grails-console",
27 | "org.grails:grails-core",
28 | "org.grails:grails-databinding",
29 | "org.grails:grails-dependencies",
30 | "org.grails:grails-docs",
31 | "org.grails:grails-encoder",
32 | "org.grails:grails-gradle-model",
33 | "org.grails:grails-logging",
34 | "org.grails:grails-plugin-codecs",
35 | "org.grails:grails-plugin-controllers",
36 | "org.grails:grails-plugin-databinding",
37 | "org.grails:grails-plugin-datasource",
38 | "org.grails:grails-plugin-domain-class",
39 | "org.grails:grails-plugin-i18n",
40 | "org.grails:grails-plugin-interceptors",
41 | "org.grails:grails-plugin-mimetypes",
42 | "org.grails:grails-plugin-rest",
43 | "org.grails:grails-plugin-services",
44 | "org.grails:grails-plugin-url-mappings",
45 | "org.grails:grails-plugin-url-validation",
46 | "org.grails:grails-shell",
47 | "org.grails:grails-spring",
48 | "org.grails:grails-test",
49 | "org.grails:grails-validation",
50 | "org.grails:grails-web",
51 | "org.grails:grails-web-boot",
52 | "org.grails:grails-web-common",
53 | "org.grails:grails-web-databinding",
54 | "org.grails:grails-web-fileupload",
55 | "org.grails:grails-web-mvc",
56 | "org.grails:grails-web-url-mappings"
57 | ],
58 | "groupName": "grails monorepo"
59 | }
60 | ]
61 | }
--------------------------------------------------------------------------------
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | name: "Java CI"
2 | on:
3 | push:
4 | branches:
5 | - '[4-9]+.[0-9]+.x'
6 | pull_request:
7 | branches:
8 | - '[4-9]+.[0-9]+.x'
9 | workflow_dispatch:
10 | jobs:
11 | test_project:
12 | name: "Test Project"
13 | runs-on: ubuntu-24.04
14 | strategy:
15 | fail-fast: true
16 | matrix:
17 | java: [17, 21]
18 | steps:
19 | - name: "📥 Checkout repository"
20 | uses: actions/checkout@v4
21 | - name: "☕️ Setup JDK"
22 | uses: actions/setup-java@v4
23 | with:
24 | java-version: ${{ matrix.java }}
25 | distribution: liberica
26 | - name: "🐘 Setup Gradle"
27 | uses: gradle/actions/setup-gradle@v4
28 | with:
29 | develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
30 | - name: "🏃 Run tests"
31 | run: ./gradlew check
32 | - name: "🏃 Run integration tests"
33 | working-directory: ./examples/testapp1
34 | run: ./gradlew integrationTest
35 | publish_snapshot:
36 | if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
37 | name: "Build Project and Publish Snapshot release"
38 | needs: test_project
39 | runs-on: ubuntu-24.04
40 | permissions:
41 | contents: write # updates gh-pages branch
42 | packages: write # publishes snapshot to GitHub Packages
43 | steps:
44 | - name: "📥 Checkout repository"
45 | uses: actions/checkout@v4
46 | - name: "☕️ Setup JDK"
47 | uses: actions/setup-java@v4
48 | with:
49 | java-version: 17
50 | distribution: liberica
51 | - name: "🐘 Setup Gradle"
52 | uses: gradle/actions/setup-gradle@v4
53 | with:
54 | develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
55 | - name: "🔨 Build Project"
56 | run: ./gradlew build
57 | - name: "📤 Publish Snapshot version to Artifactory (repo.grails.org)"
58 | env:
59 | GRAILS_PUBLISH_RELEASE: 'false'
60 | MAVEN_PUBLISH_USERNAME: ${{ secrets.MAVEN_PUBLISH_USERNAME }}
61 | MAVEN_PUBLISH_PASSWORD: ${{ secrets.MAVEN_PUBLISH_PASSWORD }}
62 | MAVEN_PUBLISH_URL: 'https://repo.grails.org/artifactory/plugins3-snapshots-local'
63 | run: ./gradlew publish
64 | - name: "📖 Generate Snapshot Documentation"
65 | run: ./gradlew docs
66 | - name: "📤 Publish Snapshot Documentation to Github Pages"
67 | uses: apache/grails-github-actions/deploy-github-pages@asf
68 | env:
69 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70 | GRADLE_PUBLISH_RELEASE: 'false'
71 | SOURCE_FOLDER: build/docs
--------------------------------------------------------------------------------
/.github/workflows/release-notes.yml:
--------------------------------------------------------------------------------
1 | name: "Release Drafter"
2 | on:
3 | issues:
4 | types: [closed, reopened]
5 | push:
6 | branches:
7 | - master
8 | - '[4-9]+.[0-9]+.x'
9 | pull_request:
10 | types: [opened, reopened, synchronize]
11 | pull_request_target:
12 | types: [opened, reopened, synchronize]
13 | jobs:
14 | update_release_draft:
15 | permissions:
16 | contents: write
17 | pull-requests: write
18 | runs-on: ubuntu-24.04
19 | steps:
20 | - name: "📝 Update Release Draft"
21 | uses: release-drafter/release-drafter@v6
22 | env:
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: "Release"
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | release:
7 | name: "Publish Release"
8 | runs-on: ubuntu-24.04
9 | permissions:
10 | packages: read # for pre-release workflow
11 | contents: write # to commit changes related to the release and publish documentation to gh-pages
12 | issues: write # to modify milestones
13 | steps:
14 | - name: "📥 Checkout repository"
15 | uses: actions/checkout@v4
16 | - name: "☕️ Setup JDK"
17 | uses: actions/setup-java@v4
18 | with:
19 | java-version: 17
20 | distribution: liberica
21 | - name: "🐘 Setup Gradle"
22 | uses: gradle/actions/setup-gradle@v4
23 | with:
24 | develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
25 | - name: "📝 Store the current release version"
26 | run: |
27 | echo "Release version: ${GITHUB_REF:11}"
28 | echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
29 | - name: "⚙ Run pre-release"
30 | uses: apache/grails-github-actions/pre-release@asf
31 | - name: "🔐 Generate key file for artifact signing"
32 | env:
33 | SECRING_FILE: ${{ secrets.SECRING_FILE }}
34 | run: echo $SECRING_FILE | base64 -d > ${{ github.workspace }}/secring.gpg
35 | - name: "📤 Publish artifacts to Sonatype"
36 | env:
37 | GRAILS_PUBLISH_RELEASE: 'true'
38 | NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }}
39 | NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }}
40 | NEXUS_PUBLISH_URL: ${{ secrets.NEXUS_PUBLISH_RELEASE_URL }}
41 | NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.NEXUS_PUBLISH_STAGING_PROFILE_ID }}
42 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
43 | SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }}
44 | run: >
45 | ./gradlew
46 | -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg
47 | publishToSonatype
48 | closeAndReleaseSonatypeStagingRepository
49 | - name: "📖 Generate Documentation"
50 | run: ./gradlew docs
51 | - name: "📤 Publish Documentation to Github Pages"
52 | uses: apache/grails-github-actions/deploy-github-pages@asf
53 | env:
54 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 | GRADLE_PUBLISH_RELEASE: 'true'
56 | SOURCE_FOLDER: build/docs
57 | VERSION: ${{ env.RELEASE_VERSION }}
58 | - name: "⚙️ Run post-release"
59 | uses: apache/grails-github-actions/post-release@asf
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Eclipse
2 | .classpath
3 | .project
4 | .gradle/
5 | build/
6 | .settings/
7 | target-eclipse/
8 |
9 | # Intellij
10 | .idea/
11 | *.iml
12 | *.iws
13 |
14 | *.log
15 | test/reports
16 | target
17 | .DS_Store
18 | plugin.xml
19 | *.zip
20 | web-app
21 |
22 |
--------------------------------------------------------------------------------
/.sdkmanrc:
--------------------------------------------------------------------------------
1 | java=17.0.14-librca
--------------------------------------------------------------------------------
/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 2009 SpringSource
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 | */
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Grails Mail Plugin
2 |
3 | [](https://central.sonatype.com/artifact/org.grails.plugins/grails-mail)
4 | [](https://github.com/grails-plugins/grails-mail/actions/workflows/gradle.yml)
5 |
6 | The Grails Mail Plugin provides a convenient Domain-Specific Language (DSL) for _sending_ email, supporting features such as plain text, HTML, attachments, inline resources and i18n (internationalization), among others.
7 |
8 | Email can be sent using the `mailService.sendMail` method. Here's an example:
9 | ```groovy
10 | mailService.sendMail {
11 | to 'fred@gmail.com', 'ginger@gmail.com'
12 | from 'john@gmail.com'
13 | cc 'marge@gmail.com', 'ed@gmail.com'
14 | bcc 'joe@gmail.com'
15 | subject 'Hello John'
16 | text 'this is some text'
17 | }
18 | ```
19 |
20 | ## Documentation
21 |
22 | - [Latest Release](https://grails-plugins.github.io/grails-mail/latest/)
23 | - [Development Snapshot](https://grails-plugins.github.io/grails-mail/snapshot/)
24 |
25 | ## Versions
26 |
27 | | Branch | Grails Version |
28 | |--------|----------------|
29 | | 1.x | 2 |
30 | | 2.x | 3 |
31 | | 3.x | 4-5 |
32 | | 4.x | 6 |
33 | | 5.x | 7 |
34 |
35 | ## Issues
36 |
37 | Issues can be raised via [GitHub Issues](https://github.com/grails-plugins/grails-mail/issues).
38 |
39 | ## Contributing
40 |
41 | Pull requests are the preferred way to submit contributions. Before submitting, please create an issue using the [issue tracker](https://github.com/grails-plugins/grails-mail/issues), outlining the problem your contribution addresses.
42 |
43 | For documentation contributions, creating an issue is not required.
44 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | version = projectVersion
2 | group = 'org.grails.plugins'
3 |
4 | apply plugin: 'groovy'
5 | apply plugin: 'java-library'
6 | apply plugin: 'org.apache.grails.gradle.grails-plugin'
7 | apply plugin: 'org.apache.grails.gradle.grails-publish'
8 |
9 | repositories {
10 | mavenCentral()
11 | maven { url = 'https://repo.grails.org/grails/core' }
12 | maven { url = 'https://repository.apache.org/content/repositories/snapshots' }
13 | }
14 |
15 | dependencies {
16 |
17 | implementation(platform("org.apache.grails:grails-bom:$grailsVersion"))
18 |
19 | api 'org.apache.grails:grails-gsp', {
20 | // api: GroovyPageTemplate, GroovyPagesTemplateEngine
21 | }
22 | api 'org.apache.grails.web:grails-web-common', {
23 | // api: GroovyPagesUriService
24 | // impl: GrailsWebRequest, WrappedResponseHolder
25 | }
26 | api 'org.springframework:spring-beans', {
27 | // api: DisposableBean, InitializingBean
28 | // impl: @Autowired(runtime), @Qualifier(runtime)
29 | }
30 | api 'org.springframework:spring-context', {
31 | // api: ApplicationContext, JndiObjectFactoryBean
32 | // impl: @Bean(runtime)
33 | }
34 | api 'org.springframework:spring-context-support', {
35 | // api: JavaMailSender, MailMessage, MailSender, SimpleMailMessage
36 | // impl: JavaMailSenderImpl, MimeMailMessage, MimeMessageHelper
37 | }
38 | api 'org.springframework:spring-core', {
39 | // api: InputStreamSource
40 | // impl: Assert, ByteArrayResource, FileSystemResource, PropertiesPropertySource, StringUtils
41 | }
42 |
43 | implementation 'org.apache.grails.views:grails-web-taglib', {
44 | // for taglib support
45 | }
46 | implementation 'org.apache.grails.web:grails-web-url-mappings', {
47 | // impl: LinkGenerator
48 | }
49 | implementation 'org.apache.groovy:groovy-templates', {
50 | // impl: Template
51 | }
52 | implementation 'org.eclipse.angus:jakarta.mail', {
53 | // impl: SMTPMessage
54 | }
55 | implementation 'org.springframework:spring-web', {
56 | // impl: RequestContextHolder
57 | }
58 | implementation 'org.springframework:spring-webmvc', {
59 | // impl: DispatcherServlet, FixedLocaleResolver, RequestContextUtils
60 | }
61 | implementation 'org.springframework.boot:spring-boot', {
62 | // impl: Bindable, Binder, @ConfigurationProperties, ConfigurationPropertySources,
63 | // @EnableConfigurationProperties(runtime)
64 | }
65 | implementation 'org.springframework.boot:spring-boot-autoconfigure', {
66 | // impl: @AutoConfiguration(runtime), @ConditionalOnMissingBean(runtime), @ConditionalOnProperty(runtime)
67 | }
68 |
69 | compileOnlyApi 'jakarta.mail:jakarta.mail-api', {
70 | // api: Message, Session
71 | // impl: MimeMessage, MimeUtility
72 | }
73 |
74 | compileOnly 'jakarta.inject:jakarta.inject-api', { // Used by Spring DI
75 | // impl: @Inject(runtime)
76 | }
77 | compileOnly 'jakarta.servlet:jakarta.servlet-api', { // Provided by the servlet container
78 | // impl: Cookie, HttpServletResponse, HttpServletRequest
79 | }
80 | compileOnly 'org.apache.grails:grails-core', { // Provided as this is a Grails plugin
81 | // api: Config, GrailsApplication, GrailsPluginManager, Plugin
82 | // impl: @Artefact(runtime), @Enhances(runtime)
83 | }
84 | compileOnly 'org.apache.groovy:groovy' // Provided as this is a Grails plugin
85 |
86 | testImplementation 'jakarta.servlet:jakarta.servlet-api', {
87 | // impl: ServletContext
88 | }
89 | testImplementation 'org.apache.grails.testing:grails-testing-support-core', {
90 | // impl: GrailsUnitTest
91 | }
92 | testImplementation 'org.spockframework:spock-core'
93 |
94 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
95 | }
96 |
97 | grailsPublish {
98 | githubSlug = 'grails-plugins/grails-mail'
99 | license {
100 | name = 'Apache-2.0'
101 | }
102 | title = 'Grails Mail Plugin'
103 | desc = 'Provides Mail support to a running Grails application'
104 | developers = [
105 | candrews: 'Craig Andrews',
106 | ldaley: 'Luke Daley',
107 | pledbrook: 'Peter Ledbrook',
108 | jeffscottbrown: 'Jeff Brown',
109 | graemerocher: 'Graeme Rocher',
110 | marcpalmer: 'Marc Palmer',
111 | sbglasius: 'Søren Berg Glasius',
112 | matrei: 'Mattias Reichel'
113 | ]
114 | }
115 |
116 | tasks.withType(Test).configureEach {
117 | useJUnitPlatform()
118 | testLogging {
119 | events 'passed', 'skipped', 'failed'
120 | }
121 | }
122 |
123 | compileJava.options.release = javaVersion.toInteger()
124 |
125 | apply from: layout.projectDirectory.file('gradle/docs-config.gradle')
126 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'groovy-gradle-plugin'
3 | }
4 |
5 | file('../gradle.properties').withInputStream {
6 | def gradleProperties = new Properties()
7 | gradleProperties.load(it)
8 | gradleProperties.each { k, v -> ext.set(k, v) }
9 |
10 | }
11 |
12 | repositories {
13 | maven { url = 'https://repo.grails.org/grails/core' }
14 | maven { url = 'https://repository.apache.org/content/repositories/snapshots' }
15 | }
16 |
17 | dependencies {
18 | implementation platform("org.apache.grails:grails-bom:$grailsVersion")
19 | implementation "org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:$asciidoctorGradlePluginVersion"
20 | implementation 'org.apache.grails:grails-gradle-plugins'
21 | }
--------------------------------------------------------------------------------
/examples/testapp1/.gitignore:
--------------------------------------------------------------------------------
1 | ### Gradle ###
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Other ###
40 | Thumbs.db
41 | .DS_Store
42 | target/
43 |
--------------------------------------------------------------------------------
/examples/testapp1/.sdkmanrc:
--------------------------------------------------------------------------------
1 | java=17.0.14-librca
--------------------------------------------------------------------------------
/examples/testapp1/build.gradle:
--------------------------------------------------------------------------------
1 | file('../../gradle.properties').withInputStream {
2 | // Inherit versions from main project
3 | def mainProjectProps = new Properties()
4 | mainProjectProps.load(it)
5 | mainProjectProps
6 | .findAll { !findProperty(it.key.toString()) }
7 | .findAll {it.key.toString().endsWith('Version') }
8 | .each { k, v -> ext.set(k, v) }
9 | }
10 |
11 | version = '0.1'
12 | group = 'testapp1'
13 |
14 | apply plugin: 'org.apache.grails.gradle.grails-web'
15 | apply plugin: 'com.bertramlabs.asset-pipeline'
16 |
17 | repositories {
18 | maven { url = 'https://repo.grails.org/grails/core' }
19 | maven { url = 'https://repository.apache.org/content/repositories/snapshots' }
20 | }
21 |
22 | dependencies {
23 |
24 | implementation platform("org.apache.grails:grails-bom:$grailsVersion")
25 |
26 | implementation "com.icegreen:greenmail:$greenmailVersion"
27 | implementation 'org.grails.plugins:grails-mail'
28 | implementation 'org.apache.grails:grails-core'
29 | implementation 'org.apache.grails:grails-controllers'
30 | implementation 'org.apache.grails:grails-gsp'
31 |
32 | testAndDevelopmentOnly 'org.webjars.npm:bootstrap'
33 | testAndDevelopmentOnly 'org.webjars.npm:bootstrap-icons'
34 | testAndDevelopmentOnly 'org.webjars.npm:jquery'
35 |
36 | runtimeOnly 'com.bertramlabs.plugins:asset-pipeline-grails'
37 | runtimeOnly 'org.fusesource.jansi:jansi'
38 | runtimeOnly 'org.apache.grails:grails-services'
39 | runtimeOnly 'org.apache.grails:grails-i18n'
40 | runtimeOnly 'org.apache.grails:grails-url-mappings'
41 | runtimeOnly 'org.springframework.boot:spring-boot-autoconfigure'
42 | runtimeOnly 'org.springframework.boot:spring-boot-starter-logging'
43 | runtimeOnly 'org.springframework.boot:spring-boot-starter-tomcat'
44 |
45 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
46 |
47 | integrationTestImplementation testFixtures('org.apache.grails:grails-geb')
48 | integrationTestImplementation 'org.apache.grails.testing:grails-testing-support-core'
49 | integrationTestImplementation 'org.spockframework:spock-core'
50 |
51 | }
52 |
53 | compileJava.options.release = javaVersion.toInteger()
54 |
55 | tasks.withType(Test).configureEach {
56 | useJUnitPlatform()
57 | testLogging {
58 | events 'passed', 'skipped', 'failed'
59 | }
60 | }
--------------------------------------------------------------------------------
/examples/testapp1/buildSrc/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'groovy-gradle-plugin'
3 | }
4 |
5 | file('../../../gradle.properties').withInputStream {
6 | def gradleProperties = new Properties()
7 | gradleProperties.load(it)
8 | gradleProperties.each { k, v -> ext.set(k, v) }
9 | }
10 |
11 | repositories {
12 | maven { url = 'https://repo.grails.org/grails/core' }
13 | maven { url = 'https://repository.apache.org/content/repositories/snapshots' }
14 | }
15 |
16 | dependencies {
17 | implementation platform("org.apache.grails:grails-bom:$grailsVersion")
18 | implementation 'com.bertramlabs.plugins:asset-pipeline-gradle'
19 | implementation 'org.apache.grails:grails-gradle-plugins'
20 | }
--------------------------------------------------------------------------------
/examples/testapp1/gradle.properties:
--------------------------------------------------------------------------------
1 | # More versions are inherited from ../../gradle.properties
2 | greenmailVersion=2.1.2
3 |
4 | org.gradle.caching=true
5 | org.gradle.daemon=true
6 | org.gradle.parallel=true
7 |
--------------------------------------------------------------------------------
/examples/testapp1/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grails-plugins/grails-mail/62073062a1ad9e57900f50dc3fd076cd860ed1f9/examples/testapp1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/examples/testapp1/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/examples/testapp1/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/examples/testapp1/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/advancedgrails.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/apple-touch-icon-retina.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grails-plugins/grails-mail/62073062a1ad9e57900f50dc3fd076cd860ed1f9/examples/testapp1/grails-app/assets/images/apple-touch-icon-retina.png
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grails-plugins/grails-mail/62073062a1ad9e57900f50dc3fd076cd860ed1f9/examples/testapp1/grails-app/assets/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/documentation.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grails-plugins/grails-mail/62073062a1ad9e57900f50dc3fd076cd860ed1f9/examples/testapp1/grails-app/assets/images/favicon.ico
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/grails-cupsonly-logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/grails.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | grails
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/images/slack.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | slack_orange
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js.
2 | //
3 | // Any JavaScript file within this directory can be referenced here using a relative path.
4 | //
5 | // You're free to add application-wide JavaScript to this file, but it's generally better
6 | // to create separate JavaScript files as needed.
7 | //
8 | //= require webjars/jquery/3.7.1/dist/jquery.js
9 | //= require webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle
10 | //= require_self
11 |
12 | if (typeof jQuery !== 'undefined') {
13 | (function($) {
14 | $('#spinner').ajaxStart(function() {
15 | $(this).fadeIn();
16 | }).ajaxStop(function() {
17 | $(this).fadeOut();
18 | });
19 | })(jQuery);
20 | }
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS file within this directory can be referenced here using a relative path.
6 | *
7 | * You're free to add application-wide styles to this file and they'll appear at the top of the
8 | * compiled file, but it's generally better to create a new file per style scope.
9 | *
10 | *= require webjars/bootstrap/5.3.3/dist/css/bootstrap
11 | *= require webjars/bootstrap-icons/1.11.3/font/bootstrap-icons
12 | *= require grails
13 | *= require_self
14 | */
15 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/stylesheets/errors.css:
--------------------------------------------------------------------------------
1 | .filename {
2 | font-style: italic;
3 | }
4 |
5 | .exceptionMessage {
6 | margin: 10px;
7 | border: 1px solid #000;
8 | padding: 5px;
9 | background-color: #E9E9E9;
10 | }
11 |
12 | .stack,
13 | .snippet {
14 | margin: 10px 0;
15 | }
16 |
17 | .stack,
18 | .snippet {
19 | border: 1px solid #ccc;
20 | }
21 |
22 | /* error details */
23 | .error-details {
24 | border: 1px solid #FFAAAA;
25 | background-color:#FFF3F3;
26 | line-height: 1.5;
27 | overflow: hidden;
28 | padding: 10px 0 5px 25px;
29 | }
30 |
31 | .error-details dt {
32 | clear: left;
33 | float: left;
34 | font-weight: bold;
35 | margin-right: 5px;
36 | }
37 |
38 | .error-details dt:after {
39 | content: ":";
40 | }
41 |
42 | .error-details dd {
43 | display: block;
44 | }
45 |
46 | /* stack trace */
47 | .stack {
48 | padding: 5px;
49 | overflow: auto;
50 | height: 300px;
51 | }
52 |
53 | /* code snippet */
54 | .snippet {
55 | background-color: #fff;
56 | font-family: monospace;
57 | }
58 |
59 | .snippet .line {
60 | display: block;
61 | }
62 |
63 | .snippet .lineNumber {
64 | background-color: #ddd;
65 | color: #999;
66 | display: inline-block;
67 | margin-right: 5px;
68 | padding: 0 3px;
69 | text-align: right;
70 | width: 3em;
71 | }
72 |
73 | .snippet .error {
74 | background-color: #fff3f3;
75 | font-weight: bold;
76 | }
77 |
78 | .snippet .error .lineNumber {
79 | background-color: #faa;
80 | color: #333;
81 | font-weight: bold;
82 | }
83 |
84 | .snippet .line:first-child .lineNumber {
85 | padding-top: 5px;
86 | }
87 |
88 | .snippet .line:last-child .lineNumber {
89 | padding-bottom: 5px;
90 | }
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/assets/stylesheets/grails.css:
--------------------------------------------------------------------------------
1 | table.scaffold tr>td:first-child, tr>th:first-child {
2 | padding-left: 1.25em;
3 | }
4 |
5 | table.scaffold tr>td:last-child, tr>th:last-child {
6 | padding-right: 1.25em;
7 | }
8 |
9 | table.scaffold th {
10 | background-image: linear-gradient(
11 | to bottom,
12 | #ffffff 0%,
13 | #f8f8f8 30%,
14 | #eaeaea 70%,
15 | #d4d4d4 100%
16 | );
17 | border-bottom: 2px solid #b3b3b3; /* Adding a subtle shadow effect */
18 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1); /* Adding a drop shadow */
19 | }
20 |
21 | [data-bs-theme=dark] table.scaffold th {
22 | background-image: linear-gradient(
23 | to bottom,
24 | #4a4a4a 0%,
25 | #3e3e3e 30%,
26 | #2a2a2a 70%,
27 | #1e1e1e 100%
28 | );
29 | border-bottom: 2px solid #141414; /* Adding a subtle shadow effect */
30 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3); /* Adding a drop shadow */
31 | }
32 |
33 | table.scaffold thead th {
34 | white-space: nowrap;
35 | }
36 |
37 | table.scaffold th a {
38 | display: block;
39 | text-decoration: none;
40 | }
41 |
42 | table.scaffold th a:link, th a:visited {
43 | color: #666666;
44 | }
45 |
46 | table.scaffold th a:hover, th a:focus {
47 | color: #333333;
48 | }
49 |
50 | table.scaffold th.sortable a {
51 | background-position: right;
52 | background-repeat: no-repeat;
53 | padding-right: 1.1em;
54 | }
55 |
56 | table.scaffold th {
57 | position: relative;
58 | }
59 |
60 |
61 | table.scaffold th.asc a:after {
62 | content: '▲';
63 | position: absolute;
64 | right: 10px;
65 | font-size: 0.8em;
66 | }
67 |
68 | table.scaffold th.desc a:after {
69 | content: '▼';
70 | position: absolute;
71 | right: 10px;
72 | font-size: 0.8em;
73 | }
74 |
75 | table.scaffold th:hover {
76 | background: #f5f5f5 !important;
77 | }
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/conf/application.yml:
--------------------------------------------------------------------------------
1 | info:
2 | app:
3 | name: '@info.app.name@'
4 | version: '@info.app.version@'
5 | grailsVersion: '@info.app.grailsVersion@'
6 | grails:
7 | mail:
8 | host: 127.0.0.1
9 | port: 3025
10 | views:
11 | default:
12 | codec: html
13 | gsp:
14 | encoding: UTF-8
15 | htmlcodec: xml
16 | codecs:
17 | expression: html
18 | scriptlet: html
19 | taglib: none
20 | staticparts: none
21 | mime:
22 | disable:
23 | accept:
24 | header:
25 | userAgents:
26 | - Gecko
27 | - WebKit
28 | - Presto
29 | - Trident
30 | types:
31 | all: '*/*'
32 | atom: application/atom+xml
33 | css: text/css
34 | csv: text/csv
35 | form: application/x-www-form-urlencoded
36 | html:
37 | - text/html
38 | - application/xhtml+xml
39 | js: text/javascript
40 | json:
41 | - application/json
42 | - text/json
43 | multipartForm: multipart/form-data
44 | pdf: application/pdf
45 | rss: application/rss+xml
46 | text: text/plain
47 | hal:
48 | - application/hal+json
49 | - application/hal+xml
50 | xml:
51 | - text/xml
52 | - application/xml
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/conf/logback-spring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | true
7 |
8 | ${CONSOLE_LOG_THRESHOLD}
9 |
10 |
11 | ${CONSOLE_LOG_PATTERN}
12 | ${CONSOLE_LOG_CHARSET}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/controllers/testapp1/GreenMailController.groovy:
--------------------------------------------------------------------------------
1 | package testapp1
2 |
3 | import com.icegreen.greenmail.util.GreenMailUtil
4 |
5 | class GreenMailController {
6 |
7 | GreenMailService greenMailService
8 |
9 | def index() {
10 | def greenMail = greenMailService.greenMail
11 | if (!greenMail) {
12 | render 'GreenMail is not running'
13 | return
14 | }
15 | def messages = greenMail.receivedMessages.collect { msg ->
16 | [
17 | from : msg.from.address,
18 | to : msg.allRecipients.join(', '),
19 | subject: msg.subject,
20 | body : GreenMailUtil.getBody(msg)
21 | ]
22 | }
23 | render(view: 'greenMail', model: [messages: messages])
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/controllers/testapp1/SendMailController.groovy:
--------------------------------------------------------------------------------
1 | package testapp1
2 |
3 | class SendMailController {
4 |
5 | def mailService
6 |
7 | def index() {
8 | mailService.sendMail {
9 | to params.to
10 | from params.from
11 | subject params.subject
12 | text params.text
13 | }
14 | redirect(controller: 'greenMail', action: 'index')
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/controllers/testapp1/UrlMappings.groovy:
--------------------------------------------------------------------------------
1 | package testapp1
2 |
3 | class UrlMappings {
4 | static mappings = {
5 | "/$controller/$action?/$id?(.$format)?"{
6 | constraints {
7 | // apply constraints here
8 | }
9 | }
10 |
11 | "/"(view:"/index")
12 | "500"(view:'/error')
13 | "404"(view:'/notFound')
14 |
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/i18n/messages.properties:
--------------------------------------------------------------------------------
1 | translate=Translate this: {0}
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/i18n/messages_fr.properties:
--------------------------------------------------------------------------------
1 | translate=Traduis ceci: {0}
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/init/testapp1/Application.groovy:
--------------------------------------------------------------------------------
1 | package testapp1
2 |
3 | import grails.boot.GrailsApp
4 | import grails.boot.config.GrailsAutoConfiguration
5 | import groovy.transform.CompileStatic
6 |
7 | @CompileStatic
8 | class Application extends GrailsAutoConfiguration {
9 | static void main(String[] args) {
10 | GrailsApp.run(Application, args)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/init/testapp1/BootStrap.groovy:
--------------------------------------------------------------------------------
1 | package testapp1
2 |
3 | class BootStrap {
4 |
5 | GreenMailService greenMailService
6 |
7 | def init = {
8 | greenMailService.start()
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/services/testapp1/GreenMailService.groovy:
--------------------------------------------------------------------------------
1 | package testapp1
2 |
3 | import com.icegreen.greenmail.util.GreenMail
4 | import com.icegreen.greenmail.util.ServerSetupTest
5 | import groovy.util.logging.Slf4j
6 | import jakarta.annotation.PreDestroy
7 |
8 | @Slf4j
9 | class GreenMailService {
10 |
11 | GreenMail greenMail
12 |
13 | void start() {
14 | greenMail = new GreenMail(ServerSetupTest.SMTP)
15 | greenMail.start()
16 | log.info('Greenmail started on port {}', greenMail.getSmtp().getPort())
17 | }
18 |
19 | void reset() {
20 | if (greenMail) {
21 | greenMail.reset()
22 | log.debug('Greenmail reset')
23 | }
24 | }
25 |
26 | @PreDestroy
27 | void stop() {
28 | if (greenMail) {
29 | greenMail.stop()
30 | log.info('Greenmail stopped')
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/_testemails/i18ntest.gsp:
--------------------------------------------------------------------------------
1 | <%@ page contentType="text/html" %>
2 |
3 |
4 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/_testemails/newLineTagTest.gsp:
--------------------------------------------------------------------------------
1 | ${part1} ${part2}
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/_testemails/newLineTest.gsp:
--------------------------------------------------------------------------------
1 | ${part1}
2 | ${part2}
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/_testemails/tagtest.gsp:
--------------------------------------------------------------------------------
1 | <%@ page contentType="text/plain" %>
2 | Condition is true
3 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/_testemails/test.gsp:
--------------------------------------------------------------------------------
1 | <%@ page contentType="text/plain" %>
2 | Message is: ${msg}
3 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/_testemails/testhtml.gsp:
--------------------------------------------------------------------------------
1 | <%@ page contentType="text/html" %>
2 | Message is: ${msg}
3 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/error.gsp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Grails Runtime Exception Error
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | An error has occurred
20 | Exception: ${exception}
21 | Message: ${message}
22 | Path: ${path}
23 |
24 |
25 |
26 |
27 |
28 | An error has occurred
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/greenMail/greenMail.gsp:
--------------------------------------------------------------------------------
1 |
2 |
3 | Greenmail Inbox
4 |
17 |
18 |
19 | 📬 Greenmail Inbox
20 |
21 |
22 |
23 | From
24 | To
25 | Subject
26 | Body
27 |
28 |
29 |
30 | ${msg.from}
31 | ${msg.to}
32 | ${msg.subject}
33 | ${msg.body}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/index.gsp:
--------------------------------------------------------------------------------
1 | <%@ page import="grails.util.Environment; org.springframework.core.SpringVersion; org.springframework.boot.SpringBootVersion"
2 | %>
3 |
4 |
5 |
6 | Welcome to Grails
7 |
8 |
9 |
10 |
11 | Application Status
12 |
31 |
32 |
33 | Artefacts
34 |
40 |
41 |
42 | Installed Plugins
43 |
48 |
49 |
50 |
51 |
56 |
57 |
58 |
59 |
60 | Welcome to Grails
61 |
62 |
63 | Congratulations, you have successfully started your first Grails application! At the moment
64 | this is the default page, feel free to modify it to either redirect to a controller or display
65 | whatever content you may choose. Below is a list of controllers that are currently deployed in
66 | this application, click on each to execute its default action:
67 |
68 |
69 |
70 |
Available Controllers:
71 |
72 |
73 |
74 | ${c.fullName}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/layouts/main.gsp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
71 |
72 |
73 |
74 | Loading...
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-app/views/notFound.gsp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Page Not Found
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Error: Page Not Found (404)
14 |
Path: ${request.forwardURI}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/testapp1/grails-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grails-plugins/grails-mail/62073062a1ad9e57900f50dc3fd076cd860ed1f9/examples/testapp1/grails-wrapper.jar
--------------------------------------------------------------------------------
/examples/testapp1/grailsw:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Grails start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRAILS_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS='"-XX:+TieredCompilation" "-XX:TieredStopAtLevel=1" "-XX:CICompilerCount=3"'
11 |
12 |
13 | # Use the maximum available, or set MAX_FD != -1 to use that value.
14 | MAX_FD="maximum"
15 |
16 | warn ( ) {
17 | echo "$*"
18 | }
19 |
20 | die ( ) {
21 | echo
22 | echo "$*"
23 | echo
24 | exit 1
25 | }
26 |
27 | # OS specific support (must be 'true' or 'false').
28 | cygwin=false
29 | msys=false
30 | darwin=false
31 | case "`uname`" in
32 | CYGWIN* )
33 | cygwin=true
34 | ;;
35 | Darwin* )
36 | darwin=true
37 | ;;
38 | MINGW* )
39 | msys=true
40 | ;;
41 | esac
42 |
43 | # Attempt to set APP_HOME
44 | # Resolve links: $0 may be a link
45 | PRG="$0"
46 | # Need this for relative symlinks.
47 | while [ -h "$PRG" ] ; do
48 | ls=`ls -ld "$PRG"`
49 | link=`expr "$ls" : '.*-> \(.*\)$'`
50 | if expr "$link" : '/.*' > /dev/null; then
51 | PRG="$link"
52 | else
53 | PRG=`dirname "$PRG"`"/$link"
54 | fi
55 | done
56 | SAVED="`pwd`"
57 | cd "`dirname \"$PRG\"`/" >/dev/null
58 | APP_HOME="`pwd -P`"
59 | cd "$SAVED" >/dev/null
60 |
61 | JAR_PATH=$APP_HOME/grails-wrapper.jar
62 |
63 | # Determine the Java command to use to start the JVM.
64 | if [ -n "$JAVA_HOME" ] ; then
65 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
66 | # IBM's JDK on AIX uses strange locations for the executables
67 | JAVACMD="$JAVA_HOME/jre/sh/java"
68 | else
69 | JAVACMD="$JAVA_HOME/bin/java"
70 | fi
71 | if [ ! -x "$JAVACMD" ] ; then
72 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
73 |
74 | Please set the JAVA_HOME variable in your environment to match the
75 | location of your Java installation."
76 | fi
77 | else
78 | JAVACMD="java"
79 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 |
85 | # Increase the maximum file descriptors if we can.
86 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
87 | MAX_FD_LIMIT=`ulimit -H -n`
88 | if [ $? -eq 0 ] ; then
89 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
90 | MAX_FD="$MAX_FD_LIMIT"
91 | fi
92 | ulimit -n $MAX_FD
93 | if [ $? -ne 0 ] ; then
94 | warn "Could not set maximum file descriptor limit: $MAX_FD"
95 | fi
96 | else
97 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
98 | fi
99 | fi
100 |
101 | # For Cygwin, switch paths to Windows format before running java
102 | if $cygwin ; then
103 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
104 | JAVACMD=`cygpath --unix "$JAVACMD"`
105 | JAR_PATH=`cygpath --path --mixed "$JAR_PATH"`
106 |
107 | # We build the pattern for arguments to be converted via cygpath
108 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
109 | SEP=""
110 | for dir in $ROOTDIRSRAW ; do
111 | ROOTDIRS="$ROOTDIRS$SEP$dir"
112 | SEP="|"
113 | done
114 | OURCYGPATTERN="(^($ROOTDIRS))"
115 | # Add a user-defined pattern to the cygpath arguments
116 | if [ "$GRAILS_CYGPATTERN" != "" ] ; then
117 | OURCYGPATTERN="$OURCYGPATTERN|($GRAILS_CYGPATTERN)"
118 | fi
119 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
120 | i=0
121 | for arg in "$@" ; do
122 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
123 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
124 |
125 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
126 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
127 | else
128 | eval `echo args$i`="\"$arg\""
129 | fi
130 | i=$((i+1))
131 | done
132 | case $i in
133 | (0) set -- ;;
134 | (1) set -- "$args0" ;;
135 | (2) set -- "$args0" "$args1" ;;
136 | (3) set -- "$args0" "$args1" "$args2" ;;
137 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
138 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
139 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
140 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
141 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
142 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
143 | esac
144 | fi
145 |
146 | # Split up the JVM_OPTS And GRAILS_OPTS values into an array, following the shell quoting and substitution rules
147 | function splitJvmOpts() {
148 | JVM_OPTS=("$@")
149 | }
150 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRAILS_OPTS
151 |
152 | exec "$JAVACMD" -jar "${JVM_OPTS[@]}" "$JAR_PATH" "$@"
153 |
--------------------------------------------------------------------------------
/examples/testapp1/grailsw.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Grails startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRAILS_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS="-XX:+TieredCompilation" "-XX:TieredStopAtLevel=1" "-XX:CICompilerCount=3"
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 | set JAR_PATH=%APP_HOME%/grails-wrapper.jar
72 |
73 | @rem Execute Grails
74 | "%JAVA_EXE%" -jar %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRAILS_OPTS% %JAR_PATH% %CMD_LINE_ARGS%
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRAILS_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRAILS_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/examples/testapp1/settings.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.gradle.develocity' version '3.19.2'
3 | id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.1'
4 | }
5 |
6 | def isCI = System.getenv('CI') != null
7 | def isLocal = !isCI
8 | def isAuthenticated = System.getenv('DEVELOCITY_ACCESS_KEY') != null
9 |
10 | develocity {
11 | server = 'https://ge.grails.org'
12 | buildScan {
13 | tag('grails-plugins')
14 | tag('grails-mail')
15 | publishing.onlyIf { isAuthenticated }
16 | uploadInBackground = isLocal
17 | }
18 | }
19 |
20 | buildCache {
21 | local { enabled = isLocal }
22 | remote(develocity.buildCache) {
23 | push = isCI && isAuthenticated
24 | enabled = true
25 | }
26 | }
27 |
28 | rootProject.name = 'testapp1'
29 |
30 | includeBuild('../../../grails-mail') {
31 | dependencySubstitution {
32 | substitute module('org.grails.plugins:grails-mail') using project(':')
33 | }
34 | }
--------------------------------------------------------------------------------
/examples/testapp1/src/integration-test/groovy/grails/plugins/mail/functional/MailSendSpec.groovy:
--------------------------------------------------------------------------------
1 | package grails.plugins.mail.functional
2 |
3 | import grails.plugin.geb.ContainerGebSpec
4 | import grails.testing.mixin.integration.Integration
5 | import testapp1.GreenMailService
6 |
7 | @Integration
8 | class MailSendSpec extends ContainerGebSpec {
9 |
10 | GreenMailService greenMailService
11 |
12 | def cleanup() {
13 | greenMailService.reset()
14 | }
15 |
16 | void 'controller should be able to send mail'() {
17 | given:
18 | def mailDetails = [
19 | from: 'abc@123.com',
20 | to: '123@abc.com',
21 | subject: 'Test subject',
22 | text: 'Test body'
23 | ]
24 |
25 | when:
26 | go "/sendMail?${toParams(mailDetails)}"
27 |
28 | then:
29 | pageSource.contains('123@abc.com')
30 | pageSource.contains('abc@123.com')
31 | pageSource.contains('Test subject')
32 | pageSource.contains('Test body')
33 | }
34 |
35 | private static String toParams(Map params) {
36 | params.collect { k, v ->
37 | "${URLEncoder.encode(k.toString(), 'UTF-8')}=${URLEncoder.encode(v.toString(), 'UTF-8')}"
38 | }.join('&')
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/testapp1/src/integration-test/resources/assets/grailslogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grails-plugins/grails-mail/62073062a1ad9e57900f50dc3fd076cd860ed1f9/examples/testapp1/src/integration-test/resources/assets/grailslogo.png
--------------------------------------------------------------------------------
/examples/testapp1/src/integration-test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UTF-8
10 | %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | projectVersion=5.0.1-SNAPSHOT
2 |
3 | grailsVersion=7.0.0-SNAPSHOT
4 | javaVersion=17
5 |
6 | asciidoctorGradlePluginVersion=4.0.4
7 |
8 | # This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs
9 | # https://github.com/grails/grails-gradle-plugin/issues/222
10 | slf4jPreventExclusion=true
11 |
12 | org.gradle.caching=true
13 | org.gradle.daemon=true
14 | org.gradle.parallel=true
15 |
--------------------------------------------------------------------------------
/gradle/docs-config.gradle:
--------------------------------------------------------------------------------
1 | import org.asciidoctor.gradle.jvm.AsciidoctorTask
2 |
3 | apply plugin: 'org.asciidoctor.jvm.convert'
4 |
5 | tasks.withType(Groovydoc).configureEach {
6 | access = GroovydocAccess.PROTECTED
7 | processScripts = false
8 | includeMainForScripts = false
9 | includeAuthor = false
10 | destinationDir = layout.buildDirectory.dir('docs/api').get().asFile
11 | }
12 |
13 | tasks.withType(AsciidoctorTask).configureEach {
14 | baseDir = layout.projectDirectory.dir('src/docs')
15 | sourceDir = layout.projectDirectory.dir('src/docs')
16 | outputDir = layout.buildDirectory.dir('docs/manual')
17 | sources {
18 | include 'index.adoc'
19 | }
20 | jvm {
21 | jvmArgs += [
22 | '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED',
23 | '--add-opens', 'java.base/java.io=ALL-UNNAMED'
24 | ]
25 | }
26 | attributes = [
27 | copyright : 'Apache License, Version 2.0',
28 | docinfo1 : 'true',
29 | doctype : 'book',
30 | encoding : 'utf-8',
31 | icons : 'font',
32 | id : "$rootProject.name:$projectVersion",
33 | idprefix : '',
34 | idseparator : '-',
35 | lang : 'en',
36 | linkattrs : true,
37 | numbered : '',
38 | producer : 'Asciidoctor',
39 | revnumber : projectVersion,
40 | setanchors : true,
41 | 'source-highlighter' : 'prettify',
42 | toc : 'left',
43 | toc2 : '',
44 | toclevels : '2',
45 | projectVersion : projectVersion
46 | ]
47 | }
48 |
49 | tasks.register('docs') {
50 | group = 'documentation'
51 | def outputFile = layout.buildDirectory.file('docs/index.html')
52 | inputs.files(tasks.named('asciidoctor'), tasks.named('groovydoc'))
53 | outputs.file(outputFile)
54 | doLast {
55 | File redirectPage = outputFile.get().asFile
56 | redirectPage.delete()
57 | redirectPage.text = '''
58 |
59 |
60 | Redirecting...
61 |
62 |
63 |
64 |
65 | '''.stripIndent(8)
66 | }
67 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grails-plugins/grails-mail/62073062a1ad9e57900f50dc3fd076cd860ed1f9/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
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, 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 | org.gradle.wrapper.GradleWrapperMain \
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=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.gradle.develocity' version '4.0'
3 | id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.2.1'
4 | }
5 |
6 | def isCI = System.getenv().containsKey('CI')
7 | def isLocal = !isCI
8 | def isAuthenticated = System.getenv().containsKey('DEVELOCITY_ACCESS_KEY')
9 |
10 | develocity {
11 | server = 'https://ge.grails.org'
12 | buildScan {
13 | tag('grails-plugins')
14 | tag('grails-mail')
15 | publishing.onlyIf { isAuthenticated }
16 | uploadInBackground = isLocal
17 | }
18 | }
19 |
20 | buildCache {
21 | local { enabled = isLocal }
22 | remote(develocity.buildCache) {
23 | push = isCI && isAuthenticated
24 | enabled = true
25 | }
26 | }
27 |
28 | rootProject.name = 'grails-mail'
29 |
--------------------------------------------------------------------------------
/src/docs/index.adoc:
--------------------------------------------------------------------------------
1 | = Grails Mail Plugin
2 |
3 | [#introduction]
4 | == Introduction
5 |
6 | The Grails Mail Plugin provides a convenient DSL for _sending_ email. It supports plain-text, html, attachments, inline
7 | resources and i18n among other features.
8 |
9 | Email can be sent using the `mailService` via the `sendMail` method.
10 |
11 | Here is an example…
12 |
13 | [source,groovy]
14 | ----
15 | mailService.sendMail {
16 | to 'fred@gmail.com', 'ginger@gmail.com'
17 | from 'john@gmail.com'
18 | cc 'marge@gmail.com', 'ed@gmail.com'
19 | bcc 'joe@gmail.com'
20 | subject 'Hello John'
21 | text 'Here it some text'
22 | }
23 | ----
24 |
25 | Here we are sending a plain-text email with no attachments to the given addresses.
26 |
27 | The `sendMail` method is injected into all Controllers to simplify access:
28 |
29 | [source,groovy]
30 | ----
31 | sendMail {
32 | to 'fred@g2one.com'
33 | subject 'Hello Fred'
34 | text 'How are you?'
35 | }
36 | ----
37 |
38 | [#configuration]
39 | == Configuration
40 |
41 | [#smtpConfiguration]
42 | === SMTP Server Configuration
43 |
44 | By default, the plugin assumes an unsecured mail server configured on port 25, getting the SMTP host name from the
45 | environment variable SMTP_HOST. However, you can change this via the `grails-app/conf/application.groovy` file.
46 |
47 | Here is an example how you would configure the default sender to send with a Gmail account:
48 |
49 | [source,groovy]
50 | ----
51 | grails {
52 | mail {
53 | host = 'smtp.gmail.com'
54 | port = 465
55 | username = 'youracount@gmail.com'
56 | password = 'yourpassword'
57 | props = [
58 | 'mail.smtp.auth': 'true',
59 | 'mail.smtp.socketFactory.port': '465',
60 | 'mail.smtp.socketFactory.class': 'javax.net.ssl.SSLSocketFactory',
61 | 'mail.smtp.socketFactory.fallback': 'false'
62 | ]
63 | }
64 | }
65 | ----
66 |
67 | And the configuration for sending via a Hotmail/Live account:
68 |
69 | [source,groovy]
70 | ----
71 | grails {
72 | mail {
73 | host = 'smtp.live.com'
74 | port = 587
75 | username = 'youracount@live.com'
76 | password = 'yourpassword'
77 | props = [
78 | 'mail.smtp.starttls.enable': 'true',
79 | 'mail.smtp.port': '587'
80 | ]
81 | }
82 | }
83 | ----
84 |
85 | If your mail session is provided via JNDI, you can use the `jndiName` setting:
86 |
87 | [source,groovy]
88 | ----
89 | grails.mail.jndiName = 'myMailSession'
90 | ----
91 |
92 | #[configuration-defaults]
93 | === Configuration Defaults
94 |
95 | You can set various _default_ settings via the application configuration that will be used in the absence of explicit
96 | values when sending email.
97 |
98 | You can also set the default "from" address to use for messages in `application.groovy` using:
99 |
100 | [source,groovy]
101 | ----
102 | grails.mail.default.from = 'server@yourhost.com'
103 | ----
104 |
105 | You can also set the default "to" address to use for messages in `application.groovy` using:
106 |
107 | [source,groovy]
108 | ----
109 | grails.mail.default.to = 'user@yourhost.com'
110 | ----
111 |
112 | [#disablingSending]
113 | === Disabling Email Sending
114 |
115 | You can completely disable the sending of email by setting:
116 |
117 | [source,groovy]
118 | ----
119 | grails.mail.disabled = true
120 | ----
121 |
122 | You may want to set this value for the development and/or test environments. However, this will treat any call to
123 | `mailService.sendMail()` as a no-op which means that the mail plugin will not attempt to render the email message or
124 | assemble any attachments. This can hide issues such as incorrect view names, invalid or non-existent configuration
125 | during development.
126 |
127 | Consider using the https://github.com/gpc/greenmail[Greenmail Plugin] which allows you to start an in memory test SMTP
128 | server for local development. This allows you to test more of your code.
129 |
130 | [#overridingAddresses]
131 | === Overriding Addresses
132 |
133 | An alternative to disabling email or using something like the http://www.grails.org/plugin/greenmail[Greenmail Plugin]
134 | , is to use the `overrideAddress` config setting for your development and/or test environment to force all email to be
135 | delivered to a specific address, regardless of what the addresses were at send time:
136 |
137 | [source,groovy]
138 | ----
139 | grails.mail.overrideAddress = 'test@address.com'
140 | ----
141 |
142 | [#sendingEmails]
143 | == Sending Emails
144 |
145 | Email is sent using the `sendMail()` method of the `mailService`. This plugin also adds a shortcut `sendMail()` method
146 | to all Controllers and Services in your application that simply delegates to the `mailService`. There is no difference
147 | between the two methods so the choice is stylistic.
148 |
149 | [source,groovy]
150 | ----
151 | class PersonController {
152 |
153 | def create = {
154 | // create user
155 |
156 | sendMail {
157 | from 'admin@mysystem.com'
158 | subject 'New user'
159 | text 'A new user has been created'
160 | }
161 | }
162 | }
163 | ----
164 |
165 | [source,groovy]
166 | ----
167 | class PersonController {
168 |
169 | def mailService
170 |
171 | def create = {
172 | // create user
173 |
174 | mailService.sendMail {
175 | from 'admin@mysystem.com'
176 | subject 'New user'
177 | text 'A new user has been created'
178 | }
179 | }
180 | }
181 | ----
182 |
183 | The `sendMail()` method takes a single `Closure` argument that uses a DSL (Domain Specific Language) to configure the
184 | message to be sent. This section describes the aspects of the DSL.include::3.1 Sender And Recipient.adoc[]
185 |
186 | [#messageContent]
187 | == Message Content
188 |
189 | Message content is specified by either the `text` and/or `html` methods that specify either the plain-text or HTML
190 | content respectively.
191 |
192 | NOTE: As of version 1.0, the `body` method that could be used to specify the message content has been deprecated.
193 | The `body` method requires the user to specify the content type of the message using a GSP directive such as:
194 |
195 | ----
196 | <%@ page contentType="text/html" %>
197 | ----
198 |
199 |
200 | === HTML Email
201 |
202 | To send HTML email you can use the `html` method. This will set the content type of the message to `text/html`.
203 |
204 | You can either supply a string value:
205 |
206 | [source,groovy]
207 | ----
208 | sendMail {
209 | to 'user@somewhere.org'
210 | subject 'Hello John'
211 | html 'Hello World'
212 | }
213 | ----
214 |
215 | Or a view to render to form the content:
216 |
217 | [source,groovy]
218 | ----
219 | sendMail {
220 | to 'user@somewhere.org'
221 | subject 'Hello John'
222 | html view: '/emails/hello', model: [param1: 'value1', param2: 'value2']
223 | }
224 | ----
225 |
226 | See the section on using views for more details of the parameters to this version of `html`.
227 |
228 |
229 | === Text Email (plain-text)
230 |
231 | To send plain-text email you can use the `text` method. This will set the content type of the message to `text/plain`.
232 |
233 | You can either supply a String value:
234 |
235 | [source,groovy]
236 | ----
237 | sendMail {
238 | to 'user@somewhere.org'
239 | subject 'Hello John'
240 | text 'Hello World'
241 | }
242 | ----
243 |
244 | Or a view to render to form the content:
245 |
246 | [source,groovy]
247 | ----
248 | sendMail {
249 | to 'user@somewhere.org'
250 | subject 'Hello John'
251 | text view: '/emails/hello', model: [param1: 'value1', param2: 'value2']
252 | }
253 | ----
254 |
255 | See the section on using views for more details of the parameters to this version of `text`.
256 |
257 |
258 | === Text and HTML
259 |
260 | It is possible to send a multipart message that contains both plain-text and HTML versions of the message. In this
261 | situation, the email-reading client is responsible for selecting the variant to display to the user.
262 |
263 | To do this, simply use both the `html` and `text` methods:
264 |
265 | [source,groovy]
266 | ----
267 | sendMail {
268 | to 'user@somewhere.org'
269 | subject 'Hello John'
270 | text view: '/emails/text-hello', model: [param1: 'value1', param2: 'value2']
271 | html view: '/emails/html-hello', model: [param1: 'value1', param2: 'value2']
272 | }
273 | ----
274 |
275 |
276 | === Using Views
277 |
278 | Both the `text` and `html` methods support specifying a view to render to form the content. These are the accepted parameters:
279 |
280 | * The _view_ is the absolute path (or relative to the current controller if during a request) to the GSP, just like the
281 | existing Grails `render` method.
282 |
283 | * The _plugin_ parameter is only necessary if the view you wish to render comes from a plugin, just like the existing
284 | Grails `render` method.
285 |
286 | * The _model_ parameter is a map representing the model the GSP will see for rendering data, just like the existing
287 | Grails `render` method.
288 |
289 |
290 | [#attachments]
291 | == Attachments
292 |
293 | The Mail Plugin is capable of adding attachments to messages as independent files and inline resources.
294 | To enable attachment support, you *MUST* indicate that the message is to be _multipart_ as the *first* thing you do in
295 | the mail DSL.
296 |
297 | [source,groovy]
298 | ----
299 | sendMail {
300 | multipart true
301 | }
302 | ----
303 |
304 |
305 | === File Attachments
306 |
307 | The term file attachments here, refers to the attachment being received as a file, not necessarily using a file in your
308 | application to form the attachment.
309 |
310 | The following methods are available in the mail DSL to attach files:
311 |
312 | [source,groovy]
313 | ----
314 | // Bytes
315 | attach(String fileName, String contentType, byte[] bytes)
316 |
317 | // Files
318 | attach(File file)
319 | attach(String fileName, File file)
320 | attach(String fileName, String contentType, File file)
321 |
322 | // InputStream
323 | attach(String fileName, String contentType, InputStreamSource source)
324 | ----
325 |
326 | There are 3 things that need to be provided when creating a file attachment:
327 |
328 | * file name - what the email client will call the file
329 | * content type - what mime type the email client will treat the file as
330 | * content source - the actual attachment
331 |
332 | The Mail Plugin supports using either a `byte[]`, `File`, or https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/InputStreamSource.html[`InputStreamSource`] as the content source.
333 |
334 | In the case of the variants that take a `File` that do not specify a file name, the name of the file will be used.
335 |
336 | In the case of the variants that take a `File` that do not specify a content type, the content type will be guessed based on the file extension.
337 |
338 | [source,groovy]
339 | ----
340 | sendMail {
341 | multipart true
342 | to 'someone@org.com'
343 | attach 'yourfile.txt', 'text/plain', 'Hello!' as byte[]
344 | }
345 | ----
346 |
347 |
348 | === Inline Attachments
349 |
350 | It is also possible to attach content as inline resources. This is particularly useful in the case of html email where
351 | you wish to embed images in the message. In this case you specify a _content id_ instead of a file name for the attachment
352 | and then reference this content id in your mail message.
353 |
354 | To attach an image as an inline resource you could do:
355 |
356 | [source,groovy]
357 | ----
358 | sendMail {
359 | multipart true
360 | to 'someone@org.com'
361 | inline 'logo', 'image/jpeg', new File('logo.jpg')
362 | html view: '/email/welcome'
363 | }
364 | ----
365 |
366 | Then in your view you reference the inline attachment using the `cid:` (content id) namespace:
367 |
368 | [source,html]
369 | ----
370 |
371 |
372 |
373 | Welcome Aboard!
374 |
375 |
376 | ----
377 |
378 | The following methods are available in the mail DSL to inline-attach files:
379 |
380 | [source,groovy]
381 | ----
382 | // Bytes
383 | inline(String fileName, String contentType, byte[] bytes)
384 |
385 | // Files
386 | inline(File file)
387 | inline(String fileName, File file)
388 | inline(String fileName, String contentType, File file)
389 |
390 | // InputStream
391 | inline(String fileName, String contentType, InputStreamSource source)
392 | ----
393 |
394 | There are 3 things that need to be provided when creating an inline attachment:
395 |
396 | * content id - the identifier of the resource
397 | * content type - what mime type the email client will treat the content as
398 | * content source - the actual content
399 |
400 | The Mail Plugin supports using either a `byte[]`, `File`, or https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/InputStreamSource.html[`InputStreamSource`] as the content source.
401 |
402 | In the case of the variants that take a `File` that do not specify a content id, the name of the file will be used.
403 |
404 | In the case of the variants that take a `File` that do not specify a content type, the content type will be guessed based on the file extension.
405 |
406 |
407 | [[testing]]
408 | == Testing
409 |
410 | Typically, you don't want to actually send email as part of your automated tests. Besides wrapping all calls to `sendMail`
411 | in an environment sensitive guard (which is a very bad idea), you can use one of the following techniques to deal with this.
412 |
413 |
414 | === Disabling Email Sending
415 |
416 | You can effectively disable email sending globally in your test by setting the following value in your application for
417 | the test environment.
418 |
419 | [source,groovy]
420 | ----
421 | grails.mail.disabled = true
422 | ----
423 |
424 | This will effectively cause all calls to `sendMail()` to be a non-operation, with a warning being logged that mail is
425 | disabled. The advantage of this technique is that it is cheap. The disadvantage is that it makes it impossible to test
426 | that email would be sent and to inspect any aspects of the sent mail.
427 |
428 |
429 | === Using an Override Address
430 |
431 | You can _override_ any and all recipient email addresses in `sendMail()` calls to force messages to be delivered to a
432 | certain mailbox.
433 |
434 | [source,groovy]
435 | ----
436 | grails.mail.overrideAddress = 'test@myorg.com'
437 | ----
438 |
439 | All `to`, `cc` and `bcc` addresses will be replaced by this value if set. The advantage of this mechanism is that it
440 | allows you to test using a real SMTP server. The disadvantage is that it requires a real SMTP server and makes it
441 | difficult to test address determination logic.
442 |
443 |
444 | === Using the Greenmail plugin
445 |
446 | The preferred approach is to use the existing https://github.com/gpc/greenmail[Grails Greenmail] plugin to run an
447 | in-memory SMTP server inside your application. This allows you to fully exercise your email sending code and to inspect
448 | sent email to assert correct values for recipient addresses etc.
449 |
450 | The advantage of this approach is that it is as close as possible to real world and gives you access to the sent email
451 | in your tests. The disadvantage is that it is another dependency.
452 |
453 | Consult the documentation for the plugin for more information.
454 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/GrailsMailException.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2008 the original author or authors.
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 | package grails.plugins.mail
17 |
18 | import groovy.transform.CompileStatic
19 | import groovy.transform.InheritConstructors
20 |
21 | @InheritConstructors
22 | @CompileStatic
23 | class GrailsMailException extends RuntimeException {}
24 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailAutoConfiguration.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2025 the original author or authors.
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 | * https://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 | package grails.plugins.mail
17 |
18 | import grails.core.GrailsApplication
19 | import grails.plugins.GrailsPluginManager
20 | import grails.web.pages.GroovyPagesUriService
21 | import groovy.transform.CompileStatic
22 | import jakarta.mail.Session
23 | import org.grails.gsp.GroovyPagesTemplateEngine
24 | import org.springframework.beans.factory.annotation.Autowired
25 | import org.springframework.beans.factory.annotation.Qualifier
26 | import org.springframework.boot.autoconfigure.AutoConfiguration
27 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
28 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
29 | import org.springframework.boot.context.properties.EnableConfigurationProperties
30 | import org.springframework.context.annotation.Bean
31 | import org.springframework.jndi.JndiObjectFactoryBean
32 | import org.springframework.mail.MailSender
33 | import org.springframework.mail.javamail.JavaMailSender
34 | import org.springframework.mail.javamail.JavaMailSenderImpl
35 |
36 | @CompileStatic
37 | @AutoConfiguration
38 | @EnableConfigurationProperties(MailConfigurationProperties)
39 | class MailAutoConfiguration {
40 |
41 | @Bean
42 | @ConditionalOnMissingBean
43 | @ConditionalOnProperty(prefix = 'grails.mail', name = 'jndiName')
44 | JndiObjectFactoryBean mailSession(MailConfigurationProperties mailProperties) {
45 | def factory = new JndiObjectFactoryBean()
46 | factory.jndiName = mailProperties.jndiName
47 | return factory
48 | }
49 |
50 | @Bean
51 | @ConditionalOnMissingBean
52 | JavaMailSender mailSender(
53 | @Autowired(required = false)
54 | @Qualifier('mailSession') Session mailSession,
55 | MailConfigurationProperties mailProperties) {
56 |
57 | def mailSender = new JavaMailSenderImpl()
58 | if (mailProperties.host) {
59 | mailSender.host = mailProperties.host
60 | } else if (!mailProperties.jndiName) {
61 | def envHost = System.getenv()['SMTP_HOST']
62 | if (envHost) {
63 | mailSender.host = envHost
64 | } else {
65 | mailSender.host = 'localhost'
66 | }
67 | }
68 | if (mailProperties.encoding) {
69 | mailSender.defaultEncoding = mailProperties.encoding
70 | } else if (!mailProperties.jndiName) {
71 | mailSender.defaultEncoding = 'utf-8'
72 | }
73 | if (mailSession != null) {
74 | mailSender.session = mailSession
75 | }
76 | if (mailProperties.port) {
77 | mailSender.port = mailProperties.port
78 | }
79 | if (mailProperties.username) {
80 | mailSender.username = mailProperties.username
81 | }
82 | if (mailProperties.password) {
83 | mailSender.password = mailProperties.password
84 | }
85 | if (mailProperties.protocol) {
86 | mailSender.protocol = mailProperties.protocol
87 | }
88 | if (mailProperties.props) {
89 | mailSender.javaMailProperties = mailProperties.props
90 | }
91 | return mailSender
92 | }
93 |
94 | @Bean
95 | @ConditionalOnMissingBean
96 | MailMessageBuilderFactory mailMessageBuilderFactory(
97 | MailSender mailSender,
98 | MailMessageContentRenderer mailMessageContentRenderer) {
99 | new MailMessageBuilderFactory(mailSender, mailMessageContentRenderer)
100 | }
101 |
102 | @Bean
103 | @ConditionalOnMissingBean
104 | MailMessageContentRenderer mailMessageContentRenderer(
105 | GroovyPagesTemplateEngine groovyPagesTemplateEngine,
106 | GroovyPagesUriService groovyPagesUriService,
107 | GrailsApplication grailsApplication,
108 | GrailsPluginManager pluginManager) {
109 | new MailMessageContentRenderer(groovyPagesTemplateEngine, groovyPagesUriService, grailsApplication, pluginManager)
110 | }
111 |
112 | @Bean
113 | @ConditionalOnMissingBean
114 | MailService mailService(MailConfigurationProperties mailConfigurationProperties,
115 | MailMessageBuilderFactory mailMessageBuilderFactory) {
116 | new MailService(mailConfigurationProperties, mailMessageBuilderFactory)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailConfigurationProperties.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2025 the original author or authors.
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 | package grails.plugins.mail
17 |
18 | import org.springframework.boot.context.properties.ConfigurationProperties
19 |
20 | @ConfigurationProperties(PREFIX)
21 | class MailConfigurationProperties {
22 |
23 | public static final String PREFIX = 'grails.mail'
24 |
25 | boolean disabled
26 | String overrideAddress
27 | Defaults defaults = new Defaults()
28 |
29 | Integer poolSize
30 | String encoding
31 | String jndiName
32 | String protocol
33 | String host
34 | Integer port
35 | String username
36 | String password
37 | Properties props
38 |
39 | /**
40 | * We can't use 'default' as a groovy property name as it is a reserved keyword.
41 | * But it is possible to define a getter for it to make it compatible.
42 | */
43 | Defaults getDefault() {
44 | return defaults
45 | }
46 |
47 | static class Defaults {
48 | String from
49 | String to
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailGrailsPlugin.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2008-2025 the original author or authors.
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 | * https://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 | package grails.plugins.mail
17 |
18 | import grails.plugins.Plugin
19 |
20 | class MailGrailsPlugin extends Plugin {
21 |
22 | def grailsVersion = '7.0.0 > *'
23 | def author = 'The Grails team'
24 | def authorEmail = 'info@grails.org'
25 | def title = 'Provides Mail support to a running Grails application'
26 | def description = '''\
27 | This plugin provides a MailService class as well as configuring the necessary beans within
28 | the Spring ApplicationContext.
29 |
30 | It also adds a "sendMail" method to all controller classes. A typical example usage is:
31 |
32 | sendMail {
33 | to 'fred@g2one.com','ginger@g2one.com'
34 | from 'john@g2one.com'
35 | cc 'marge@g2one.com', 'ed@g2one.com'
36 | bcc 'joe@g2one.com'
37 | subject 'Hello John'
38 | text 'this is some text'
39 | }
40 | '''.stripIndent(8)
41 | def documentation = 'https://grails-plugins.github.io/grails-mail'
42 | def license = 'Apache 2.0 License'
43 | def organization = [name: 'Grails Plugins', url: 'https://github.com/grails-plugins']
44 | def developers = [
45 | [name: 'Craig Andrews', email: 'candrews@integralblue.com'],
46 | [name: 'Luke Daley', email: 'ld@ldaley.com'],
47 | [name: 'Peter Ledbrook', email: 'p.ledbrook@cacoethes.co.uk'],
48 | [name: 'Jeff Brown', email: 'brownj@ociweb.com'],
49 | [name: 'Graeme Rocher', email: 'rocherg@ociweb.com'],
50 | [name: 'Marc Palmer', email: 'marc@grailsrocks.com'],
51 | [name: 'Søren Berg Glasius', email: 'soeren@glasius.dk'],
52 | [name: 'Mattias Reichel', email: 'mattias.reichel@gmail.com']
53 | ]
54 | def issueManagement = [
55 | system: 'GitHub',
56 | url: 'https://github.com/grails-plugins/grails-mail/issues'
57 | ]
58 | def scm = [
59 | url: 'https://github.com/grails-plugins/grails-mail'
60 | ]
61 | def providedArtefacts = [PlainTextMailTagLib]
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailMessageBuilder.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2008-2025 the original author or authors.
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 | * https://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 | package grails.plugins.mail
17 |
18 | import groovy.transform.CompileDynamic
19 | import groovy.transform.CompileStatic
20 | import groovy.util.logging.Slf4j
21 | import org.eclipse.angus.mail.smtp.SMTPMessage
22 | import org.springframework.core.io.ByteArrayResource
23 | import org.springframework.core.io.FileSystemResource
24 | import org.springframework.core.io.InputStreamSource
25 | import org.springframework.mail.MailMessage
26 | import org.springframework.mail.MailSender
27 | import org.springframework.mail.SimpleMailMessage
28 | import org.springframework.mail.javamail.JavaMailSender
29 | import org.springframework.mail.javamail.MimeMailMessage
30 | import org.springframework.mail.javamail.MimeMessageHelper
31 | import org.springframework.util.Assert
32 | import org.springframework.util.StringUtils
33 |
34 | import jakarta.mail.Message
35 | import jakarta.mail.internet.MimeMessage
36 | import jakarta.mail.internet.MimeUtility
37 | import java.util.concurrent.ExecutorService
38 |
39 | /**
40 | * Provides a DSL style interface to mail message sending/generation.
41 | *
42 | * If the builder is constructed without a MailMessageContentRenderer, it is incapable
43 | * of rendering GSP views into message content.
44 | */
45 | @Slf4j
46 | @CompileStatic
47 | class MailMessageBuilder {
48 |
49 | final MailSender mailSender
50 | final MailMessageContentRenderer mailMessageContentRenderer
51 |
52 | final String defaultFrom
53 | final String defaultTo
54 | final String overrideAddress
55 |
56 | private MailMessage message
57 | private MimeMessageHelper helper
58 | private Locale locale
59 |
60 | private String textContent
61 | private String htmlContent
62 | private String envelopeFrom
63 |
64 | private int multipart = MimeMessageHelper.MULTIPART_MODE_NO
65 | private boolean async = false
66 |
67 | private List inlines = []
68 |
69 | private static class Inline {
70 | String id
71 | String contentType
72 | InputStreamSource toAdd
73 | }
74 |
75 | MailMessageBuilder(MailSender mailSender, MailConfigurationProperties properties,
76 | MailMessageContentRenderer mailMessageContentRenderer = null) {
77 | this.mailSender = mailSender
78 | this.mailMessageContentRenderer = mailMessageContentRenderer
79 | this.overrideAddress = properties.overrideAddress
80 | this.defaultFrom = overrideAddress ?: properties.default.from
81 | this.defaultTo = overrideAddress ?: properties.default.to
82 | }
83 |
84 | private MailMessage getMessage() {
85 | if (!message) {
86 | if (mimeCapable) {
87 | helper = new MimeMessageHelper((mailSender as JavaMailSender).createMimeMessage(), multipart)
88 | message = new MimeMailMessage(helper)
89 | } else {
90 | message = new SimpleMailMessage()
91 | }
92 | if (defaultFrom) {
93 | message.from = defaultFrom
94 | }
95 | if (defaultTo) {
96 | message.setTo(defaultTo)
97 | }
98 | }
99 | return message
100 | }
101 |
102 | MailMessage sendMessage(ExecutorService executorService) {
103 | def message = finishMessage()
104 |
105 | log.trace(
106 | 'Sending mail {} ...',
107 | message instanceof MimeMailMessage ?
108 | getDescription((MimeMailMessage) message) :
109 | getDescription((Message) message)
110 | )
111 |
112 | def sendingMsg = (message instanceof MimeMailMessage) ? message.mimeMessage : message
113 |
114 | if (envelopeFrom) {
115 | if (!mimeCapable) {
116 | throw new GrailsMailException('You must use a JavaMailSender to set the envelopeFrom.')
117 | }
118 | sendingMsg = new SMTPMessage(sendingMsg as MimeMessage)
119 | sendingMsg.envelopeFrom = envelopeFrom
120 | }
121 |
122 | if (async) {
123 | executorService.execute({
124 | try {
125 | send(sendingMsg)
126 | } catch(Throwable t) {
127 | log.error('Failed to send email', t)
128 | }
129 | } as Runnable)
130 | } else {
131 | send(sendingMsg)
132 | }
133 |
134 | log.trace(
135 | 'Sent mail {}',
136 | message instanceof MimeMailMessage ?
137 | getDescription((MimeMailMessage) message) :
138 | getDescription((Message) message)
139 | )
140 | return message
141 | }
142 |
143 | /**
144 | * Method to send messages of any type.
145 | * This method is dynamically compiled to avoid the need to cast the mail sender and message to the correct types.
146 | * @param message Any type of message
147 | */
148 | @CompileDynamic
149 | private void send(Object message) {
150 | mailSender.send(message)
151 | }
152 |
153 | void multipart(boolean multipart) {
154 | this.multipart = multipart ?
155 | MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED :
156 | MimeMessageHelper.MULTIPART_MODE_NO
157 | }
158 |
159 | void multipart(int multipartMode) {
160 | this.multipart = multipartMode
161 | }
162 |
163 | void async(boolean async) {
164 | this.async = async
165 | }
166 |
167 | void headers(Map headers) {
168 | Assert.notEmpty(headers, 'headers cannot be null')
169 |
170 | // The message must be of type MimeMailMessage to add headers.
171 | if (!mimeCapable) {
172 | throw new GrailsMailException('You must use a JavaMailSender to customise the headers.')
173 | }
174 |
175 | MailMessage msg = getMessage()
176 | if (msg instanceof MimeMailMessage) {
177 | MimeMessage mimeMessage = (msg as MimeMailMessage).mimeMessageHelper.mimeMessage
178 | headers.each { name, value ->
179 | String nameString = name?.toString()
180 | String valueString = value?.toString()
181 |
182 | Assert.hasText(nameString, 'header names cannot be null or empty')
183 | Assert.hasText(valueString, "header value for '$nameString' cannot be null")
184 |
185 | mimeMessage.setHeader(nameString, valueString)
186 | }
187 | } else {
188 | throw new GrailsMailException('Mail message builder is not mime capable so headers cannot be set')
189 | }
190 | }
191 |
192 | void to(Object[] args) {
193 | Assert.notEmpty(args, 'to cannot be null or empty')
194 | Assert.noNullElements(args, 'to cannot contain null elements')
195 | getMessage().setTo(toDestinationAddresses(args))
196 | }
197 |
198 | void to(List args) {
199 | Assert.notEmpty(args, 'to cannot be null or empty')
200 | Assert.noNullElements(args, 'to cannot contain null elements')
201 | to(args.toArray())
202 | }
203 |
204 | void bcc(Object[] args) {
205 | Assert.notEmpty(args, 'bcc cannot be null or empty')
206 | Assert.noNullElements(args, 'bcc cannot contain null elements')
207 | getMessage().setBcc(toDestinationAddresses(args))
208 | }
209 |
210 | void bcc(List args) {
211 | Assert.notEmpty(args, 'bcc cannot be null or empty')
212 | Assert.noNullElements(args, 'bcc cannot contain null elements')
213 | bcc(args.toArray())
214 | }
215 |
216 | void cc(Object[] args) {
217 | Assert.notEmpty(args, 'cc cannot be null or empty')
218 | Assert.noNullElements(args, 'cc cannot contain null elements')
219 | getMessage().setCc(toDestinationAddresses(args))
220 | }
221 |
222 | void cc(List args) {
223 | Assert.notEmpty(args, 'cc cannot be null or empty')
224 | Assert.noNullElements(args, 'cc cannot contain null elements')
225 | cc(args.toArray())
226 | }
227 |
228 | void replyTo(CharSequence replyTo) {
229 | def value = replyTo.toString()
230 | Assert.hasText(value, 'replyTo cannot be null or 0 length')
231 | getMessage().replyTo = value
232 | }
233 |
234 | void from(CharSequence from) {
235 | def value = from.toString()
236 | Assert.hasText(value, 'from cannot be null or 0 length')
237 | getMessage().from = value
238 | }
239 |
240 | void envelopeFrom(CharSequence envFrom) {
241 | def value = envFrom.toString()
242 | Assert.hasText(value, 'envelope from cannot be null or 0 length')
243 | envelopeFrom = value
244 | }
245 |
246 | void title(CharSequence title) {
247 | Assert.notNull(title, 'title cannot be null')
248 | subject(title)
249 | }
250 |
251 | void subject(CharSequence title) {
252 | Assert.notNull(title, 'subject cannot be null')
253 | getMessage().subject = title.toString()
254 | }
255 |
256 | void body(CharSequence body) {
257 | Assert.notNull(body, 'body cannot be null')
258 | text(body)
259 | }
260 |
261 | void body(Map params) {
262 | Assert.notEmpty(params, 'body cannot be null or empty')
263 | def render = doRender(params)
264 | if (render.html) {
265 | html(render.out.toString()) // @todo Spring mail helper will not set correct mime type if we give it XHTML
266 | } else {
267 | text(render.out.toString())
268 | }
269 | }
270 |
271 | protected MailMessageContentRender doRender(Map params) {
272 | if (mailMessageContentRenderer == null) {
273 | throw new GrailsMailException('mail message builder was constructed without a message content render so cannot render views')
274 | }
275 | if (!params.view) {
276 | throw new GrailsMailException('no view specified')
277 | }
278 | mailMessageContentRenderer.render(new StringWriter(), params.view as String, params.model as Map, locale, params.plugin as String)
279 | }
280 |
281 | void text(Map params) {
282 | Assert.notEmpty(params, 'params cannot be null or empty')
283 | text(doRender(params).out.toString())
284 | }
285 |
286 | void text(CharSequence text) {
287 | Assert.notNull(text, 'text cannot be null')
288 | textContent = text.toString()
289 | }
290 |
291 | void html(Map params) {
292 | Assert.notEmpty(params, 'params cannot be null or empty')
293 | html(doRender(params).out.toString())
294 | }
295 |
296 | void html(CharSequence text) {
297 | Assert.notNull(text, 'html cannot be null')
298 | if (mimeCapable) {
299 | htmlContent = text.toString()
300 | } else {
301 | throw new GrailsMailException('mail sender is not mime capable, try configuring a JavaMailSender')
302 | }
303 | }
304 |
305 | void locale(String localeStr) {
306 | Assert.hasText(localeStr, 'locale cannot be null or empty')
307 | locale(StringUtils.parseLocale(localeStr))
308 | }
309 |
310 | void locale(Locale locale) {
311 | Assert.notNull(locale, 'locale cannot be null')
312 | this.locale = locale
313 | }
314 |
315 | /**
316 | * @deprecated use attach(String, String, byte[])
317 | */
318 | void attachBytes(String fileName, String contentType, byte[] bytes) {
319 | attach(fileName, contentType, bytes)
320 | }
321 |
322 | void attach(String fileName, String contentType, byte[] bytes) {
323 | attach(fileName, contentType, new ByteArrayResource(bytes))
324 | }
325 |
326 | void attach(File file) {
327 | attach(file.name, file)
328 | }
329 |
330 | void attach(String fileName, File file) {
331 | if (!mimeCapable) {
332 | throw new GrailsMailException('Message is not an instance of org.springframework.mail.javamail.MimeMessage, cannot attach bytes!')
333 | }
334 | attach(fileName, helper.fileTypeMap.getContentType(file), file)
335 | }
336 |
337 | void attach(String fileName, String contentType, File file) {
338 | if (!file.exists()) {
339 | throw new FileNotFoundException("cannot use $file as an attachment as it does not exist")
340 | }
341 | attach(fileName, contentType, new FileSystemResource(file))
342 | }
343 |
344 | void attach(String fileName, String contentType, InputStreamSource source) {
345 | doAdd(fileName, contentType, source, true)
346 | }
347 |
348 | void inline(String contentId, String contentType, byte[] bytes) {
349 | inline(contentId, contentType, new ByteArrayResource(bytes))
350 | }
351 |
352 | void inline(File file) {
353 | inline(file.name, file)
354 | }
355 |
356 | void inline(String fileName, File file) {
357 | if (!mimeCapable) {
358 | throw new GrailsMailException('Message is not an instance of org.springframework.mail.javamail.MimeMessage, cannot attach bytes!')
359 | }
360 | inline(fileName, helper.fileTypeMap.getContentType(file), file)
361 | }
362 |
363 | void inline(String contentId, String contentType, File file) {
364 | if (!file.exists()) {
365 | throw new FileNotFoundException("cannot use $file as an attachment as it does not exist")
366 | }
367 | inline(contentId, contentType, new FileSystemResource(file))
368 | }
369 |
370 | void inline(String contentId, String contentType, InputStreamSource source) {
371 | inlines << new Inline(id: contentId, contentType: contentType, toAdd: source)
372 | }
373 |
374 | protected void doAdd(String id, String contentType, InputStreamSource toAdd, boolean isAttachment) {
375 | if (!mimeCapable) {
376 | throw new GrailsMailException('Message is not an instance of org.springframework.mail.javamail.MimeMessage, cannot attach bytes!')
377 | }
378 | assert multipart, 'message is not marked as "multipart"; use "multipart true" as the first line in your builder DSL'
379 |
380 | getMessage() // ensure that helper is initialized
381 | if (isAttachment) {
382 | helper.addAttachment(MimeUtility.encodeWord(id), toAdd, contentType)
383 | } else {
384 | helper.addInline(MimeUtility.encodeWord(id), toAdd, contentType)
385 | }
386 | }
387 |
388 | boolean isMimeCapable() {
389 | mailSender instanceof JavaMailSender
390 | }
391 |
392 | protected String[] toDestinationAddresses(addresses) {
393 | if (overrideAddress) {
394 | addresses = addresses.collect { overrideAddress }
395 | }
396 | addresses.collect { it?.toString() } as String[]
397 | }
398 |
399 | static protected String getDescription(SimpleMailMessage message) {
400 | "[${message.subject}] from [${message.from}] to ${message.to}"
401 | }
402 |
403 | static protected String getDescription(Message message) {
404 | "[${message.subject}] from [${message.from}] to ${message.getRecipients(Message.RecipientType.TO)*.toString()}"
405 | }
406 |
407 | static protected String getDescription(MimeMailMessage message) {
408 | getDescription(message.mimeMessage)
409 | }
410 |
411 | MailMessage finishMessage() {
412 | def message = getMessage()
413 | if (htmlContent) {
414 | if (textContent) {
415 | helper.setText(textContent, htmlContent)
416 | } else {
417 | helper.setText(htmlContent, true)
418 | }
419 | } else {
420 | if (!textContent) {
421 | throw new GrailsMailException('message has no content, use text(), html() or body() methods to set content')
422 | }
423 | message.text = textContent
424 | }
425 | inlines.each {
426 | doAdd(it.id, it.contentType, it.toAdd, false)
427 | }
428 | message.sentDate = new Date()
429 | return message
430 | }
431 | }
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailMessageBuilderFactory.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2008-2024 the original author or authors.
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 | package grails.plugins.mail
17 |
18 | import groovy.transform.CompileStatic
19 | import org.springframework.mail.MailSender
20 | import org.springframework.mail.javamail.JavaMailSender
21 |
22 | /**
23 | * Responsible for creating builder instances, which have dependencies and
24 | * are not threadsafe.
25 | */
26 | @CompileStatic
27 | class MailMessageBuilderFactory {
28 |
29 | MailSender mailSender
30 | MailMessageContentRenderer mailMessageContentRenderer
31 |
32 | MailMessageBuilderFactory() {}
33 |
34 | /**
35 | * @param mailSender
36 | * @param mailMessageContentRenderer
37 | * @since 4.0.0
38 | */
39 | MailMessageBuilderFactory(MailSender mailSender, MailMessageContentRenderer mailMessageContentRenderer) {
40 | this.mailSender = mailSender
41 | this.mailMessageContentRenderer = mailMessageContentRenderer
42 | }
43 |
44 | MailMessageBuilder createBuilder(MailConfigurationProperties properties) {
45 | new MailMessageBuilder(mailSender, properties, mailMessageContentRenderer)
46 | }
47 |
48 | boolean isMimeCapable() {
49 | mailSender instanceof JavaMailSender
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailMessageContentRender.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010 the original author or authors.
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 grails.plugins.mail
18 |
19 | import groovy.transform.CompileStatic
20 |
21 | /**
22 | * Represents the result of rendering a message content view.
23 | */
24 | @CompileStatic
25 | class MailMessageContentRender {
26 |
27 | private static String[] HTML_CONTENT_TYPES = ['text/html', 'text/xhtml']
28 |
29 | final Writer out
30 | final String contentType
31 |
32 | MailMessageContentRender(Writer out, String contentType) {
33 | this.out = out
34 | this.contentType = contentType
35 | }
36 |
37 | boolean isHtml() {
38 | contentType in HTML_CONTENT_TYPES
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailMessageContentRenderer.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2024 the original author or authors.
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 grails.plugins.mail
18 |
19 | import grails.core.GrailsApplication
20 | import grails.plugins.GrailsPluginManager
21 | import grails.web.mapping.LinkGenerator
22 | import grails.web.pages.GroovyPagesUriService
23 | import groovy.text.Template
24 | import groovy.transform.CompileStatic
25 | import org.grails.gsp.GroovyPageTemplate
26 | import org.grails.gsp.GroovyPagesTemplateEngine
27 | import org.grails.web.servlet.WrappedResponseHolder
28 | import org.grails.web.servlet.mvc.GrailsWebRequest
29 | import org.springframework.context.ApplicationContext
30 | import org.springframework.web.context.request.RequestContextHolder
31 | import org.springframework.web.servlet.DispatcherServlet
32 | import org.springframework.web.servlet.i18n.FixedLocaleResolver
33 | import org.springframework.web.servlet.support.RequestContextUtils
34 |
35 | import jakarta.servlet.http.Cookie
36 | import jakarta.servlet.http.HttpServletRequest
37 | import jakarta.servlet.http.HttpServletResponse
38 | import java.lang.reflect.InvocationHandler
39 | import java.lang.reflect.Method
40 | import java.lang.reflect.Proxy
41 | import java.util.concurrent.ConcurrentHashMap
42 |
43 | /**
44 | * Renders a GSP into the content of a mail message.
45 | */
46 | @CompileStatic
47 | class MailMessageContentRenderer {
48 |
49 | GroovyPagesTemplateEngine groovyPagesTemplateEngine
50 | GroovyPagesUriService groovyPagesUriService
51 | GrailsApplication grailsApplication
52 | GrailsPluginManager pluginManager
53 |
54 | MailMessageContentRenderer() {}
55 |
56 | /**
57 | * @param groovyPagesTemplateEngine The GSP template engine to use
58 | * @param groovyPagesUriService The GSP URI service to use
59 | * @param grailsApplication The Grails application
60 | * @param pluginManager The plugin manager
61 | * @since 4.0.0
62 | */
63 | MailMessageContentRenderer(GroovyPagesTemplateEngine groovyPagesTemplateEngine,
64 | GroovyPagesUriService groovyPagesUriService,
65 | GrailsApplication grailsApplication,
66 | GrailsPluginManager pluginManager) {
67 | this.groovyPagesTemplateEngine = groovyPagesTemplateEngine
68 | this.groovyPagesUriService = groovyPagesUriService
69 | this.grailsApplication = grailsApplication
70 | this.pluginManager = pluginManager
71 | }
72 |
73 | MailMessageContentRender render(Writer out, String templateName, Map model, Locale locale, String pluginName = null) {
74 | RenderEnvironment.with(grailsApplication.mainContext, out, locale) { RenderEnvironment env ->
75 | Template template = createTemplate(templateName, env.controllerName, pluginName)
76 | if (model instanceof Map) {
77 | template.make(model).writeTo(out)
78 | } else {
79 | template.make().writeTo(out)
80 | }
81 | new MailMessageContentRender(out, template.metaInfo.contentType)
82 | } as MailMessageContentRender
83 | }
84 |
85 | protected GroovyPageTemplate createTemplate(String templateName, String controllerName, String pluginName) {
86 | if (templateName.startsWith('/')) {
87 | if (!controllerName) {
88 | controllerName = ''
89 | }
90 | } else {
91 | if (!controllerName) {
92 | throw new IllegalArgumentException('Mail views cannot be loaded from relative view paths when there is no current HTTP request')
93 | }
94 | }
95 |
96 | String contextPath = getContextPath(pluginName)
97 | String templateUri = contextPath ?
98 | contextPath + groovyPagesUriService.getViewURI(controllerName, templateName) :
99 | groovyPagesUriService.getDeployedViewURI(controllerName, templateName)
100 |
101 | def template = groovyPagesTemplateEngine.createTemplateForUri(templateUri)
102 | if (!template) {
103 | if (pluginName) {
104 | throw new IllegalArgumentException("Could not locate email view ${templateName} in plugin [$pluginName]")
105 | } else {
106 | throw new IllegalArgumentException("Could not locate mail body ${templateName}. Is it in a plugin? If so you must pass the plugin name in the [plugin] variable")
107 | }
108 | }
109 | template as GroovyPageTemplate
110 | }
111 |
112 | protected String getContextPath(String pluginName) {
113 | String contextPath = ''
114 | if (pluginName) {
115 | def plugin = pluginManager.getGrailsPlugin(pluginName)
116 | if (plugin && !plugin.isBasePlugin()) {
117 | contextPath = "${plugin.pluginPath}/grails-app/views"
118 | }
119 | }
120 | contextPath
121 | }
122 |
123 | private static class RenderEnvironment {
124 |
125 | final PrintWriter out
126 | final Locale locale
127 | final ApplicationContext applicationContext
128 | final LinkGenerator grailsLinkGenerator
129 |
130 | private GrailsWebRequest originalRequestAttributes
131 | private GrailsWebRequest renderRequestAttributes
132 | private HttpServletResponse originalWrappedResponse
133 |
134 | RenderEnvironment(ApplicationContext applicationContext, Writer out, Locale locale = null) {
135 | this.out = out instanceof PrintWriter ? out as PrintWriter : new PrintWriter(out)
136 | this.locale = locale
137 | this.applicationContext = applicationContext
138 | this.grailsLinkGenerator = applicationContext.getBean('grailsLinkGenerator', LinkGenerator)
139 | }
140 |
141 | private void init() {
142 | originalRequestAttributes = RequestContextHolder.getRequestAttributes() as GrailsWebRequest
143 | originalWrappedResponse = WrappedResponseHolder.wrappedResponse
144 |
145 | def renderLocale = Locale.getDefault()
146 | if (locale) {
147 | renderLocale = locale
148 | } else if (originalRequestAttributes) {
149 | renderLocale = RequestContextUtils.getLocale(originalRequestAttributes.request)
150 | }
151 |
152 | renderRequestAttributes = new GrailsWebRequest(
153 | PageRenderRequestCreator.createInstance(grailsLinkGenerator.serverBaseURL, '/mail/render', renderLocale),
154 | PageRenderResponseCreator.createInstance(out, renderLocale),
155 | null,
156 | applicationContext
157 | )
158 |
159 | if (originalRequestAttributes) {
160 | renderRequestAttributes.controllerName = originalRequestAttributes.controllerName
161 | }
162 |
163 | RequestContextHolder.requestAttributes = renderRequestAttributes
164 | renderRequestAttributes.request.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, new FixedLocaleResolver(defaultLocale: renderLocale))
165 | renderRequestAttributes.setOut(out)
166 | WrappedResponseHolder.wrappedResponse = renderRequestAttributes.currentResponse
167 | }
168 |
169 | private void close() {
170 | RequestContextHolder.requestAttributes = originalRequestAttributes // null ok
171 | WrappedResponseHolder.wrappedResponse = originalWrappedResponse
172 | }
173 |
174 | /**
175 | * Establish an environment inheriting the locale of the current request if there is one
176 | */
177 | static Object with(ApplicationContext applicationContext, Writer out, Closure block) {
178 | with(applicationContext, out, null, block)
179 | }
180 |
181 | /**
182 | * Establish an environment with a specific locale
183 | */
184 | static Object with(ApplicationContext applicationContext, Writer out, Locale locale, Closure block) {
185 | def env = new RenderEnvironment(applicationContext, out, locale)
186 | env.init()
187 | try {
188 | block(env)
189 | } finally {
190 | env.close()
191 | }
192 | }
193 |
194 | String getControllerName() {
195 | renderRequestAttributes.controllerName
196 | }
197 | }
198 |
199 | /*
200 | * Creates the request object used during the GSP rendering pipeline for render operations outside a web request.
201 | * Created dynamically to avoid issues with different servlet API spec versions.
202 | */
203 | static class PageRenderRequestCreator {
204 |
205 | static HttpServletRequest createInstance(final String serverBaseURL, final String requestURI, Locale localeToUse = Locale.getDefault()) {
206 | final URI serverBaseURI = serverBaseURL != null ? new URI(serverBaseURL) : null
207 |
208 | def params = new ConcurrentHashMap()
209 | def attributes = new ConcurrentHashMap()
210 |
211 | String contentType = null
212 | String characterEncoding = 'UTF-8'
213 |
214 | Proxy.newProxyInstance(HttpServletRequest.classLoader, [HttpServletRequest] as Class[], new InvocationHandler() {
215 | Object invoke(proxy, Method method, Object[] args) {
216 |
217 | String methodName = method.name
218 |
219 | if (methodName == 'getContentType') {
220 | return contentType
221 | }
222 | if (methodName == 'setContentType') {
223 | contentType = args[0]
224 | return null
225 | }
226 | if (methodName == 'getCharacterEncoding') {
227 | return characterEncoding
228 | }
229 | if (methodName == 'setCharacterEncoding') {
230 | characterEncoding = args[0]
231 | return null
232 | }
233 | if (methodName == 'getRealPath') {
234 | return requestURI
235 | }
236 | if (methodName == 'getLocalName') {
237 | return 'localhost'
238 | }
239 | if (methodName == 'getLocalAddr') {
240 | return '127.0.0.1'
241 | }
242 | if (methodName == 'getLocalPort') {
243 | return 80
244 | }
245 | if (methodName == 'getCookies') {
246 | return ([] as Cookie[])
247 | }
248 | if (methodName == 'getDateHeader' || methodName == 'getIntHeader') {
249 | return -1
250 | }
251 | if (methodName == 'getMethod') {
252 | return 'GET'
253 | }
254 | if (methodName == 'getContextPath' || methodName == 'getServletPath') {
255 | return '/'
256 | }
257 | if (methodName in ['getPathInfo', 'getPathTranslated', 'getQueryString']) {
258 | return ''
259 | }
260 | if (methodName == 'getRequestURL') {
261 | return new StringBuffer(requestURI)
262 | }
263 | if (methodName == 'getRequestURI') {
264 | return requestURI
265 | }
266 | if (methodName == 'isRequestedSessionIdValid') {
267 | return true
268 | }
269 | if (methodName in [
270 | 'isRequestedSessionIdFromCookie', 'isRequestedSessionIdFromURL', 'isRequestedSessionIdFromUrl',
271 | 'authenticate', 'isUserInRole', 'isSecure', 'isAsyncStarted', 'isAsyncSupported']) {
272 | return false
273 | }
274 | if (methodName == 'getSession') {
275 | throw new UnsupportedOperationException('You cannot use the session in non-request rendering operations')
276 | }
277 | if (methodName == 'getInputStream') {
278 | throw new UnsupportedOperationException('You cannot read the input stream in non-request rendering operations')
279 | }
280 | if (methodName == 'getProtocol') {
281 | throw new UnsupportedOperationException('You cannot read the protocol in non-request rendering operations')
282 | }
283 | if (methodName == 'getScheme') {
284 | if (serverBaseURI == null) {
285 | throw new UnsupportedOperationException('You cannot read the scheme in non-request rendering operations')
286 | }
287 | return serverBaseURI.scheme
288 | }
289 | if (methodName == 'getServerName') {
290 | if(serverBaseURI == null) {
291 | throw new UnsupportedOperationException('You cannot read the servername in non-request rendering operations')
292 | }
293 | return serverBaseURI.host
294 | }
295 | if (methodName == 'getServerPort') {
296 | if (serverBaseURI == null) {
297 | throw new UnsupportedOperationException('You cannot read the server port in non-request rendering operations')
298 | }
299 | int port = serverBaseURI.port
300 | if (port == -1) {
301 | switch (serverBaseURI.scheme?.toLowerCase()) {
302 | case 'https':
303 | port = 443
304 | break
305 | default:
306 | port = 80
307 | }
308 | }
309 | return port
310 | }
311 | if (methodName == 'getReader') {
312 | throw new UnsupportedOperationException('You cannot read input in non-request rendering operations')
313 | }
314 | if (methodName == 'getRemoteAddr') {
315 | throw new UnsupportedOperationException('You cannot read the remote address in non-request rendering operations')
316 | }
317 | if (methodName == 'getRemoteHost') {
318 | throw new UnsupportedOperationException('You cannot read the remote host in non-request rendering operations')
319 | }
320 | if (methodName == 'getRequestDispatcher') {
321 | throw new UnsupportedOperationException('You cannot use the request dispatcher in non-request rendering operations')
322 | }
323 | if (methodName == 'getRemotePort') {
324 | throw new UnsupportedOperationException('You cannot read the remote port in non-request rendering operations')
325 | }
326 | if (methodName == 'getParts') {
327 | return []
328 | }
329 | if (methodName == 'getAttribute') {
330 | return attributes[args[0]]
331 | }
332 | if (methodName == 'getAttributeNames') {
333 | return attributes.keys()
334 | }
335 | if (methodName == 'setAttribute') {
336 | String name = args[0]
337 | Object o = args[1]
338 | if (o == null) {
339 | attributes.remove(name)
340 | } else {
341 | attributes[name] = o
342 | }
343 | return null
344 | }
345 | if (methodName == 'removeAttribute') {
346 | attributes.remove(args[0])
347 | return null
348 | }
349 | if (methodName == 'getLocale') {
350 | return localeToUse
351 | }
352 | if (methodName == 'getLocales') {
353 | def iterator = [localeToUse].iterator()
354 | //noinspection UnnecessaryQualifiedReference
355 | return PageRenderRequestCreator.iteratorAsEnumeration(iterator)
356 | }
357 | if (methodName == 'getParameter') {
358 | return params[args[0]]
359 | }
360 | if (methodName == 'getParameterNames') {
361 | return params.keys()
362 | }
363 | if (methodName == 'getParameterValues') {
364 | return [] as String[]
365 | }
366 | if (methodName == 'getParameterMap') {
367 | return params
368 | }
369 | if (methodName == 'getContentLength') {
370 | return 0
371 | }
372 | if ('getHeaderNames' == methodName || 'getHeaders' == methodName) {
373 | return Collections.enumeration(Collections.emptySet())
374 | }
375 | return null
376 | }
377 | }) as HttpServletRequest
378 | }
379 |
380 | private static Enumeration iteratorAsEnumeration(Iterator iterator) {
381 | new Enumeration() {
382 | @Override
383 | boolean hasMoreElements() {
384 | iterator.hasNext()
385 | }
386 |
387 | @Override
388 | Object nextElement() {
389 | iterator.next()
390 | }
391 | }
392 | }
393 | }
394 |
395 | static class PageRenderResponseCreator {
396 |
397 | static HttpServletResponse createInstance(final PrintWriter writer, Locale localeToUse = Locale.getDefault()) {
398 |
399 | String characterEncoding = 'UTF-8'
400 | String contentType = null
401 | int bufferSize = 0
402 |
403 | Proxy.newProxyInstance(HttpServletResponse.classLoader, [HttpServletResponse] as Class[], new InvocationHandler() {
404 | Object invoke(proxy, Method method, Object[] args) {
405 |
406 | String methodName = method.name
407 |
408 | if (methodName == 'getContentType') {
409 | return contentType
410 | }
411 | if (methodName == 'setContentType') {
412 | contentType = args[0]
413 | return null
414 | }
415 | if (methodName == 'getCharacterEncoding') {
416 | return characterEncoding
417 | }
418 | if (methodName == 'setCharacterEncoding') {
419 | characterEncoding = args[0]
420 | return null
421 | }
422 | if (methodName == 'getBufferSize') {
423 | return bufferSize
424 | }
425 | if (methodName == 'setBufferSize') {
426 | bufferSize = args[0] as Integer
427 | return null
428 | }
429 | if (methodName == 'containsHeader' || methodName == 'isCommitted') {
430 | return false
431 | }
432 | if (methodName in ['encodeURL', 'encodeRedirectURL', 'encodeUrl', 'encodeRedirectUrl']) {
433 | return args[0]
434 | }
435 | if (methodName == 'getWriter') {
436 | return writer
437 | }
438 | if (methodName == 'getOutputStream') {
439 | throw new UnsupportedOperationException('You cannot use the OutputStream in non-request rendering operations. Use getWriter() instead')
440 | }
441 | if (methodName == 'getHeaderNames') {
442 | return []
443 | }
444 | if (methodName == 'getLocale') {
445 | return localeToUse
446 | }
447 | if (methodName == 'getStatus') {
448 | return 0
449 | }
450 | return null
451 | }
452 | }) as HttpServletResponse
453 | }
454 | }
455 | }
456 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/MailService.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2008-2025 the original author or authors.
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 | package grails.plugins.mail
17 |
18 | import grails.config.Config
19 | import groovy.transform.CompileStatic
20 | import groovy.util.logging.Slf4j
21 | import org.springframework.beans.factory.DisposableBean
22 | import org.springframework.beans.factory.InitializingBean
23 | import org.springframework.boot.context.properties.bind.Bindable
24 | import org.springframework.boot.context.properties.bind.Binder
25 | import org.springframework.boot.context.properties.source.ConfigurationPropertySources
26 | import org.springframework.core.env.PropertiesPropertySource
27 | import org.springframework.mail.MailMessage
28 |
29 | import java.util.concurrent.LinkedBlockingQueue
30 | import java.util.concurrent.ThreadPoolExecutor
31 | import java.util.concurrent.TimeUnit
32 |
33 | /**
34 | * Provides the entry point to the mail sending API.
35 | */
36 | @Slf4j
37 | @CompileStatic
38 | class MailService implements InitializingBean, DisposableBean {
39 |
40 | private final MailConfigurationProperties mailConfigurationProperties
41 | private final MailMessageBuilderFactory mailMessageBuilderFactory
42 |
43 | private ThreadPoolExecutor mailExecutorService
44 |
45 | private static final Integer DEFAULT_POOL_SIZE = 5
46 | private static final Bindable CONFIG_BINDABLE = Bindable.of(MailConfigurationProperties)
47 |
48 | MailService(
49 | MailConfigurationProperties mailConfigurationProperties,
50 | MailMessageBuilderFactory mailMessageBuilderFactory) {
51 | this.mailConfigurationProperties = mailConfigurationProperties
52 | this.mailMessageBuilderFactory = mailMessageBuilderFactory
53 | }
54 |
55 | MailMessage sendMail(
56 | MailConfigurationProperties properties,
57 | @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = MailMessageBuilder) Closure callable) {
58 | if (disabled) {
59 | log.warn('Sending emails disabled by configuration option')
60 | return null
61 | }
62 | def messageBuilder = mailMessageBuilderFactory.createBuilder(properties)
63 | callable.delegate = messageBuilder
64 | callable.resolveStrategy = Closure.DELEGATE_FIRST
65 | callable.call(messageBuilder)
66 | return messageBuilder.sendMessage(mailExecutorService)
67 | }
68 |
69 | MailMessage sendMail(
70 | Config config,
71 | @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = MailMessageBuilder) Closure callable) {
72 | sendMail(toMailProperties(config), callable)
73 | }
74 |
75 | MailMessage sendMail(@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = MailMessageBuilder) Closure callable) {
76 | sendMail(mailConfigurationProperties, callable)
77 | }
78 |
79 | private static MailConfigurationProperties toMailProperties(Config config) {
80 | def propertySource = new PropertiesPropertySource('mailProperties', config.toProperties())
81 | def configPropertySources = ConfigurationPropertySources.from(propertySource)
82 | def binder = new Binder(configPropertySources)
83 | return binder.bind(MailConfigurationProperties.PREFIX, CONFIG_BINDABLE).get()
84 | }
85 |
86 | boolean isDisabled() {
87 | mailConfigurationProperties.disabled
88 | }
89 |
90 | void setPoolSize(Integer poolSize){
91 | mailExecutorService.setMaximumPoolSize(poolSize ?: DEFAULT_POOL_SIZE)
92 | mailExecutorService.setCorePoolSize(poolSize ?: DEFAULT_POOL_SIZE)
93 | }
94 |
95 | @Override
96 | void destroy() throws Exception {
97 | mailExecutorService.shutdown()
98 | mailExecutorService.awaitTermination(10, TimeUnit.SECONDS)
99 | }
100 |
101 | @Override
102 | void afterPropertiesSet() throws Exception {
103 | mailExecutorService = new ThreadPoolExecutor(
104 | 1,
105 | 1,
106 | 60,
107 | TimeUnit.SECONDS,
108 | new LinkedBlockingQueue()
109 | )
110 | mailExecutorService.allowCoreThreadTimeOut(true)
111 | setPoolSize(mailConfigurationProperties.poolSize)
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/PlainTextMailTagLib.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2004-2025 the original author or authors.
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 | package grails.plugins.mail
17 |
18 | import grails.artefact.Artefact
19 |
20 | @Artefact('TagLib')
21 | class PlainTextMailTagLib {
22 |
23 | static namespace = 'text'
24 |
25 | def newLine = {
26 | out << '\n'
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/groovy/grails/plugins/mail/SendMail.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 the original author or authors.
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 | package grails.plugins.mail
17 |
18 | import grails.artefact.Enhances
19 | import groovy.transform.CompileStatic
20 | import jakarta.inject.Inject
21 | import org.springframework.context.ApplicationContext
22 | import org.springframework.mail.MailMessage
23 |
24 | @CompileStatic
25 | @Enhances(['Controller', 'Service'])
26 | trait SendMail {
27 |
28 | @Inject
29 | ApplicationContext applicationContext
30 |
31 | MailMessage sendMail(@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = MailMessageBuilder) Closure dsl) {
32 | applicationContext.getBean('mailService', MailService).sendMail(dsl)
33 | }
34 | }
--------------------------------------------------------------------------------
/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
--------------------------------------------------------------------------------
1 | grails.plugins.mail.MailAutoConfiguration
--------------------------------------------------------------------------------
/src/test/groovy/grails/plugins/mail/MailMessageBuilderSpec.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2008 the original author or authors.
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 | package grails.plugins.mail
17 |
18 | import org.grails.testing.GrailsUnitTest
19 | import org.springframework.mail.MailSender
20 | import org.springframework.mail.javamail.JavaMailSender
21 | import org.springframework.web.context.support.ServletContextResource
22 | import spock.lang.Issue
23 | import spock.lang.Specification
24 |
25 | import jakarta.mail.Message
26 | import jakarta.mail.Session
27 | import jakarta.mail.internet.MimeMessage
28 | import jakarta.mail.internet.MimeMultipart
29 | import jakarta.mail.internet.MimeUtility
30 | import jakarta.servlet.ServletContext
31 |
32 | /**
33 | * Test case for {@link MailMessageBuilder}.
34 | */
35 | class MailMessageBuilderSpec extends Specification implements GrailsUnitTest {
36 |
37 | MailMessageBuilder testJavaMailSenderBuilder
38 | MailMessageBuilder testBasicMailSenderBuilder
39 |
40 | private static String defaultFrom = "from@grailsplugin.com"
41 | private static String defaultTo = "to@grailsplugin.com"
42 |
43 | def setup() {
44 | def properties = new MailConfigurationProperties()
45 | properties.default.from = defaultFrom
46 | properties.default.to = defaultTo
47 |
48 | def mockJavaMailSender = Stub(JavaMailSender) {
49 | createMimeMessage() >> new MimeMessage(Session.getInstance(new Properties()))
50 | }
51 |
52 | testJavaMailSenderBuilder = new MailMessageBuilder(mockJavaMailSender, properties)
53 |
54 | def mockBasicMailSender = Stub(MailSender)
55 | testBasicMailSenderBuilder = new MailMessageBuilder(mockBasicMailSender, properties)
56 | }
57 |
58 | /**
59 | * .
60 | */
61 | void "Tests the basic elements of the mail DSL"() {
62 | when:
63 | processDsl {
64 | to "fred@g2one.com"
65 | subject "Hello Fred"
66 | body 'How are you?'
67 | }
68 |
69 | then:
70 | MimeMessage msg = testJavaMailSenderBuilder.message.mimeMessage
71 | to(msg).size() == 1
72 | to(msg)[0].toString() == "fred@g2one.com"
73 | msg.subject == "Hello Fred"
74 | msg.content == "How are you?"
75 | }
76 |
77 | void "Tests that multiple recipients are added to the underlying mail message correctly"() {
78 | when:
79 | processDsl {
80 | to "fred@g2one.com", "ginger@g2one.com", "grace@hollywood.com"
81 | from "john@g2one.com"
82 | cc "marge@g2one.com", "ed@g2one.com"
83 | bcc "joe@g2one.com"
84 | subject "Hello John"
85 | body 'this is some text'
86 | }
87 |
88 | then:
89 | MimeMessage msg = testJavaMailSenderBuilder.message.mimeMessage
90 | to(msg) == ["fred@g2one.com", "ginger@g2one.com", "grace@hollywood.com"]
91 | cc(msg) == ["marge@g2one.com", "ed@g2one.com"]
92 | bcc(msg) == ["joe@g2one.com"]
93 | msg.from.size() == 1
94 | msg.from[0].toString() == "john@g2one.com"
95 | msg.subject == "Hello John"
96 | msg.content == "this is some text"
97 | }
98 |
99 | void "Tests the 'headers' feature of the mail DSL. It should add the specified headers to the underlying MIME message"() {
100 | when:
101 | processDsl {
102 | headers "X-Mailing-List": "user@grails.codehaus.org",
103 | "Sender": "dilbert@somewhere.org"
104 | to "fred@g2one.com"
105 | subject "Hello Fred"
106 | body 'How are you?'
107 | }
108 |
109 | then:
110 | MimeMessage msg = testJavaMailSenderBuilder.message.mimeMessage
111 | msg.getHeader("X-Mailing-List", ", ") == "user@grails.codehaus.org"
112 | msg.getHeader("Sender", ", ") == "dilbert@somewhere.org"
113 | to(msg) == ["fred@g2one.com"]
114 | msg.subject == "Hello Fred"
115 | msg.content == "How are you?"
116 | }
117 |
118 | void "Tests that the builder throws an exception if the user tries to specify custom headers with just a plain MailSender"() {
119 | when:
120 | processDsl(testBasicMailSenderBuilder) {
121 | headers "Content-Type": "text/plain;charset=UTF-8",
122 | "Sender": "dilbert@somewhere.org"
123 | to "fred@g2one.com"
124 | subject "Hello Fred"
125 | body 'How are you?'
126 | }
127 |
128 | then:
129 | thrown(GrailsMailException)
130 | }
131 |
132 | void "Test that default to and from fields works as expected"() {
133 | when:
134 | processDsl {
135 | subject "Hello Fred"
136 | body 'How are you?'
137 | }
138 | then:
139 | MimeMessage msg = testJavaMailSenderBuilder.message.mimeMessage
140 | to(msg)[0].toString() == defaultTo
141 | msg.from[0].toString() == defaultFrom
142 | }
143 |
144 | void "Test that envelopeFrom works as expected"() {
145 | when:
146 | processDsl {
147 | to "fred@g2one.com"
148 | from "john@g2one.com"
149 | envelopeFrom "peter@g2one.com"
150 | subject "Hello Fred"
151 | body 'How are you?'
152 | }
153 | then:
154 | MimeMessage msg = testJavaMailSenderBuilder.message.mimeMessage
155 | to(msg)[0].toString() == "fred@g2one.com"
156 | msg.from[0].toString() == "john@g2one.com"
157 | testJavaMailSenderBuilder.envelopeFrom == "peter@g2one.com"
158 | }
159 |
160 | void "Test that attachments works as expected"() {
161 | when:
162 | processDsl {
163 | multipart true
164 | to "fred@g2one.com"
165 | subject "Hello Fred"
166 | body 'How are you?'
167 | attachBytes "dummy.bin", "application/binary", "abcdef".bytes
168 | attachBytes "äöü.bin", "application/binary", "abcdef".bytes
169 | }
170 |
171 | then:
172 | MimeMessage msg = testJavaMailSenderBuilder.message.mimeMessage
173 | msg.content instanceof MimeMultipart
174 | 3 == msg.content.count
175 |
176 | and:
177 | def attachment1 = msg.content.getBodyPart(1)
178 | attachment1.content.text == "abcdef"
179 | attachment1.fileName == "dummy.bin"
180 |
181 | and:
182 | def attachment2 = msg.content.getBodyPart(2)
183 | attachment2.fileName == MimeUtility.encodeWord("äöü.bin")
184 | }
185 |
186 | void "Test that attaching a stream works as expected"() {
187 | setup:
188 | def servletContext = [getResourceAsStream: { new ByteArrayInputStream("abcdef".bytes) }] as ServletContext
189 |
190 | when:
191 | processDsl {
192 | multipart true
193 | to "fred@g2one.com"
194 | subject "Hello Fred"
195 | body 'How are you?'
196 | attach "dummy.bin", "application/binary", new ServletContextResource(servletContext, "path/to/file")
197 | }
198 |
199 | then:
200 | def msg = testJavaMailSenderBuilder.message.mimeMessage
201 | msg.content instanceof MimeMultipart
202 | msg.content.count == 2
203 |
204 | def attachment = msg.content.getBodyPart(1)
205 | attachment.content.text == "abcdef"
206 | attachment.fileName == "dummy.bin"
207 | }
208 |
209 | void "Test that attachment using attach file no filename override"() {
210 | setup: "create temp file to attach"
211 | def tempFile = File.createTempFile("grailsMailUnitTest", ".txt")
212 | tempFile << 'abcdef'
213 |
214 | when:
215 | processDsl {
216 | multipart true
217 | to "fred@g2one.com"
218 | subject "Hello Fred"
219 | body 'How are you?'
220 | attach tempFile
221 | }
222 | then:
223 |
224 | def msg = testJavaMailSenderBuilder.message.mimeMessage
225 | msg.content instanceof MimeMultipart
226 | msg.content.count == 2
227 |
228 | def attachment = msg.content.getBodyPart(1)
229 | attachment.content == 'abcdef'
230 | attachment.fileName == tempFile.name
231 | attachment.contentType == 'text/plain'
232 |
233 | cleanup:
234 | tempFile.delete()
235 | }
236 |
237 | void "Test attachment using attach file with filename override"() {
238 | setup:
239 | def tempFile = File.createTempFile("grailsMailUnitTest", ".txt")
240 | tempFile << 'abcdef'
241 |
242 | when:
243 | processDsl {
244 | multipart true
245 | to "fred@g2one.com"
246 | subject "Hello Fred"
247 | body 'How are you?'
248 | attach 'alternativeName.txt', tempFile
249 | }
250 | then:
251 | def msg = testJavaMailSenderBuilder.message.mimeMessage
252 | msg.content instanceof MimeMultipart
253 | msg.content.count == 2
254 |
255 | def attachment = msg.content.getBodyPart(1)
256 | attachment.content == 'abcdef'
257 | attachment.fileName == 'alternativeName.txt'
258 | attachment.contentType == 'text/plain'
259 |
260 | cleanup:
261 | tempFile.delete()
262 | }
263 |
264 | void "Test Attach File With Non Existent File"() {
265 | when:
266 | processDsl {
267 | multipart true
268 | to "fred@g2one.com"
269 | subject "Hello Fred"
270 | body 'How are you?'
271 | attach new File("I don't exist.zip")
272 | }
273 | then:
274 | thrown(FileNotFoundException)
275 | }
276 |
277 | @Issue("for issue GPMAIL-60")
278 | void "test Attach Call In Beginning Of Dsl"() {
279 | setup:
280 | def servletContext = [getResourceAsStream: { new ByteArrayInputStream("abcdef".bytes) }] as ServletContext
281 |
282 | when:
283 | processDsl {
284 | multipart true
285 | attach "dummy.bin", "application/binary", new ServletContextResource(servletContext, "path/to/file")
286 | to "fred@g2one.com"
287 | subject "Hello Fred"
288 | body 'How are you?'
289 | }
290 | then:
291 | def msg = testJavaMailSenderBuilder.message.mimeMessage
292 | msg.content instanceof MimeMultipart
293 | msg.content.count == 2
294 | msg.getHeader("To", " ") == "fred@g2one.com"
295 | msg.getHeader("Subject", " ") == "Hello Fred"
296 |
297 | and:
298 | def attachment = msg.content.getBodyPart(1)
299 | attachment.content.text == "abcdef"
300 | attachment.fileName == "dummy.bin"
301 | }
302 |
303 | private List to(MimeMessage msg) {
304 | msg.getRecipients(Message.RecipientType.TO)*.toString()
305 | }
306 |
307 | private List cc(MimeMessage msg) {
308 | msg.getRecipients(Message.RecipientType.CC)*.toString()
309 | }
310 |
311 | private List bcc(MimeMessage msg) {
312 | msg.getRecipients(Message.RecipientType.BCC)*.toString()
313 | }
314 |
315 | private processDsl(Closure c) {
316 | processDsl(testJavaMailSenderBuilder, c)
317 | }
318 |
319 | private processDsl(MailMessageBuilder builder, Closure c) {
320 | c.delegate = builder
321 | c.call()
322 | builder.finishMessage()
323 | }
324 | }
325 |
--------------------------------------------------------------------------------