Skip to content

Commit 866b171

Browse files
committed
feat: add ability to send e-mails via Mailgun API.
Important points to take into account during upgrade: - application-prod.properties: new mandatory properties: mailgun.endpoint and mailgun.password - application-prod.properties: org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration has been removed from spring.autoconfigure.exclude property - Togglz: new SEND_MAIL_VIA_HTTP_API feature that is enabled by default See for details: https://documentation.mailgun.com/en/latest/api-sending.html Fix #935
1 parent 3b3eb8e commit 866b171

File tree

11 files changed

+248
-7
lines changed

11 files changed

+248
-7
lines changed

NEWS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- (functionality) suggest a possible country on a series creation page (contributed by John Shkarin)
2727
- (functionality) suggest a possible category on a series creation page
2828
- (functionality) show similar series on a page with series info
29+
- (infrastructure) add ability to send e-mails via Mailgun API
2930

3031
0.3
3132
- (functionality) implemented possibility to user to add series to his collection

src/main/java/ru/mystamps/web/config/ServicesConfig.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import lombok.RequiredArgsConstructor;
2121
import org.slf4j.LoggerFactory;
22+
import org.springframework.boot.web.client.RestTemplateBuilder;
2223
import org.springframework.context.MessageSource;
2324
import org.springframework.context.annotation.Bean;
2425
import org.springframework.context.annotation.Configuration;
@@ -47,12 +48,16 @@
4748
import ru.mystamps.web.feature.series.importing.SeriesImportConfig;
4849
import ru.mystamps.web.feature.series.importing.sale.SeriesSalesImportConfig;
4950
import ru.mystamps.web.feature.series.sale.SeriesSalesConfig;
51+
import ru.mystamps.web.service.ApiMailgunEmailSendingStrategy;
5052
import ru.mystamps.web.service.CronService;
5153
import ru.mystamps.web.service.CronServiceImpl;
54+
import ru.mystamps.web.service.FallbackMailgunEmailSendingStrategy;
5255
import ru.mystamps.web.service.MailService;
5356
import ru.mystamps.web.service.MailServiceImpl;
57+
import ru.mystamps.web.service.MailgunEmailSendingStrategy;
5458
import ru.mystamps.web.service.SiteService;
5559
import ru.mystamps.web.service.SiteServiceImpl;
60+
import ru.mystamps.web.service.SmtpMailgunEmailSendingStrategy;
5661
import ru.mystamps.web.service.SuspiciousActivityService;
5762
import ru.mystamps.web.service.SuspiciousActivityServiceImpl;
5863

@@ -86,6 +91,7 @@ public class ServicesConfig {
8691
private final ReportService reportService;
8792
private final SeriesService seriesService;
8893
private final UserService userService;
94+
private final RestTemplateBuilder restTemplateBuilder;
8995

9096
@Lazy
9197
private final UsersActivationService usersActivationService;
@@ -137,9 +143,18 @@ public MailService getMailService() {
137143
boolean isProductionEnvironment = env.acceptsProfiles("prod");
138144
boolean enableTestMode = !isProductionEnvironment;
139145

146+
String user = "api";
147+
String password = env.getRequiredProperty("mailgun.password");
148+
String endpoint = env.getRequiredProperty("mailgun.endpoint");
149+
150+
MailgunEmailSendingStrategy mailStrategy = new FallbackMailgunEmailSendingStrategy(
151+
new ApiMailgunEmailSendingStrategy(restTemplateBuilder, endpoint, user, password),
152+
new SmtpMailgunEmailSendingStrategy(mailSender)
153+
);
154+
140155
return new MailServiceImpl(
141156
reportService,
142-
mailSender,
157+
mailStrategy,
143158
messageSource,
144159
env.getProperty("app.mail.admin.email", "root@localhost"),
145160
new Locale(env.getProperty("app.mail.admin.lang", "en")),
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright (C) 2009-2019 Slava Semushin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.service;
19+
20+
import org.apache.commons.lang3.StringUtils;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.springframework.boot.web.client.RestTemplateBuilder;
24+
import org.springframework.http.HttpEntity;
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.HttpMethod;
27+
import org.springframework.http.MediaType;
28+
import org.springframework.http.ResponseEntity;
29+
import org.springframework.util.LinkedMultiValueMap;
30+
import org.springframework.util.MultiValueMap;
31+
import org.springframework.web.client.RestClientException;
32+
import org.springframework.web.client.RestTemplate;
33+
import ru.mystamps.web.service.exception.EmailSendingException;
34+
35+
import javax.mail.internet.InternetAddress;
36+
import java.io.UnsupportedEncodingException;
37+
38+
// CheckStyle: ignore LineLength for next 10 lines
39+
/**
40+
* Sending e-mails with Mailgun service (via HTTP API).
41+
*
42+
* @see <a href="https://documentation.mailgun.com/en/latest/api-intro.html">API: Introduction</a>
43+
* @see <a href="https://documentation.mailgun.com/en/latest/api-sending.html">API: Sending</a>
44+
* @see <a href="https://documentation.mailgun.com/en/latest/user_manual.html#sending-via-api">API: Manual</a>
45+
*/
46+
public class ApiMailgunEmailSendingStrategy implements MailgunEmailSendingStrategy {
47+
48+
private static final Logger LOG = LoggerFactory.getLogger(ApiMailgunEmailSendingStrategy.class);
49+
50+
private final String endpoint;
51+
private final RestTemplate restTemplate;
52+
53+
public ApiMailgunEmailSendingStrategy(
54+
RestTemplateBuilder restTemplateBuilder,
55+
String endpoint,
56+
String user,
57+
String password) {
58+
59+
this.endpoint = endpoint;
60+
61+
this.restTemplate = restTemplateBuilder
62+
.basicAuthorization(user, password)
63+
.build();
64+
}
65+
66+
/*
67+
This method is a roughly equivalent to the following curl command:
68+
69+
$ curl -s -v \
70+
https://api.mailgun.net/v3/my-stamps.ru/messages \
71+
--user "api:$API_KEY" \
72+
-F from='My Stamps <dont-reply%my-stamps.ru>' \
73+
-F to=example%example.com \
74+
-F subject=Test \
75+
-F text=Hello \
76+
-F o:tag=test \
77+
-F o:testmode=true
78+
79+
The response example:
80+
81+
< HTTP/1.1 100 Continue
82+
< HTTP/1.1 200 OK
83+
< Access-Control-Allow-Headers: Content-Type, x-requested-with
84+
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
85+
< Access-Control-Allow-Origin: *
86+
< Access-Control-Max-Age: 600
87+
< Content-Disposition: inline
88+
< Content-Type: application/json
89+
< Date: Tue, 07 May 2019 19:42:10 GMT
90+
< Server: nginx
91+
< Strict-Transport-Security: max-age=60; includeSubDomains
92+
< X-Ratelimit-Limit: 1000000
93+
< X-Ratelimit-Remaining: 999999
94+
< X-Ratelimit-Reset: 1557258140503
95+
< X-Recipient-Limit: 1000000
96+
< X-Recipient-Remaining: 999999
97+
< X-Recipient-Reset: 1557258140500
98+
< Content-Length: 136
99+
< Connection: keep-alive
100+
<
101+
{
102+
"id": "<[email protected]>",
103+
"message": "Queued. Thank you."
104+
}
105+
*/
106+
@Override
107+
public void send(MailgunEmail email) {
108+
109+
try {
110+
InternetAddress from =
111+
new InternetAddress(email.senderAddress(), email.senderName(), "UTF-8");
112+
113+
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
114+
parts.add("from", from.toString());
115+
parts.add("to", email.recipientAddress());
116+
parts.add("subject", email.subject());
117+
parts.add("text", email.text());
118+
parts.add("o:tag", email.tag());
119+
parts.add("o:testmode", String.valueOf(email.testMode()));
120+
121+
HttpHeaders headers = new HttpHeaders();
122+
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE);
123+
headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_UTF8_VALUE);
124+
125+
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
126+
127+
ResponseEntity<String> response = restTemplate.exchange(
128+
endpoint,
129+
HttpMethod.POST,
130+
request,
131+
String.class
132+
);
133+
134+
LOG.info("Mailgun response code: {}", response.getStatusCode());
135+
LOG.debug("Mailgun response headers: {}", response.getHeaders());
136+
LOG.info("Mailgun response body: {}", StringUtils.remove(response.getBody(), '\n'));
137+
138+
} catch (UnsupportedEncodingException | RestClientException ex) {
139+
throw new EmailSendingException("Can't send mail to " + email.recipientAddress(), ex);
140+
}
141+
}
142+
143+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (C) 2009-2019 Slava Semushin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.service;
19+
20+
import lombok.RequiredArgsConstructor;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import ru.mystamps.web.support.togglz.Features;
24+
25+
@RequiredArgsConstructor
26+
public class FallbackMailgunEmailSendingStrategy implements MailgunEmailSendingStrategy {
27+
28+
private static final Logger LOG =
29+
LoggerFactory.getLogger(FallbackMailgunEmailSendingStrategy.class);
30+
31+
private final MailgunEmailSendingStrategy primaryStrategy;
32+
private final MailgunEmailSendingStrategy fallbackStrategy;
33+
34+
@Override
35+
public void send(MailgunEmail email) {
36+
boolean needFallback = usePrimaryStrategy(email);
37+
if (needFallback) {
38+
useFallbackStrategy(email);
39+
}
40+
}
41+
42+
private boolean usePrimaryStrategy(MailgunEmail email) {
43+
if (!Features.SEND_MAIL_VIA_HTTP_API.isActive()) {
44+
return true;
45+
}
46+
47+
try {
48+
primaryStrategy.send(email);
49+
return false;
50+
51+
} catch (RuntimeException ex) { // NOPMD: AvoidCatchingGenericException; try to catch-all
52+
LOG.warn(
53+
"Couldn't send email with the primary strategy, fallback to the secondary",
54+
extractCause(ex)
55+
);
56+
return true;
57+
}
58+
}
59+
60+
private void useFallbackStrategy(MailgunEmail email) {
61+
fallbackStrategy.send(email);
62+
}
63+
64+
private static Throwable extractCause(RuntimeException ex) {
65+
if (ex.getCause() != null) {
66+
return ex.getCause();
67+
}
68+
return ex;
69+
}
70+
71+
}

src/main/java/ru/mystamps/web/service/MailServiceImpl.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.slf4j.Logger;
2525
import org.slf4j.LoggerFactory;
2626
import org.springframework.context.MessageSource;
27-
import org.springframework.mail.javamail.JavaMailSender;
2827
import org.springframework.scheduling.annotation.Async;
2928
import ru.mystamps.web.Url;
3029
import ru.mystamps.web.feature.account.SendUsersActivationDto;
@@ -49,15 +48,15 @@ public class MailServiceImpl implements MailService {
4948

5049
public MailServiceImpl(
5150
ReportService reportService,
52-
JavaMailSender mailer,
51+
MailgunEmailSendingStrategy mailer,
5352
MessageSource messageSource,
5453
String adminEmail,
5554
Locale adminLang,
5655
String robotEmail,
5756
boolean testMode) {
5857

5958
this.reportService = reportService;
60-
this.mailer = new SmtpMailgunEmailSendingStrategy(mailer);
59+
this.mailer = mailer;
6160
this.messageSource = messageSource;
6261
this.adminEmail = adminEmail;
6362
this.adminLang = adminLang;

src/main/java/ru/mystamps/web/service/MailgunEmailSendingStrategy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public interface MailgunEmailSendingStrategy {
2929
*
3030
* @param email data and meta-data for sending an e-mail
3131
* @throws ru.mystamps.web.service.exception.EmailSendingException when any error occurs
32+
* @see ApiMailgunEmailSendingStrategy
3233
* @see SmtpMailgunEmailSendingStrategy
3334
* @see <a href="https://documentation.mailgun.com/en/latest/user_manual.html#tagging">Tagging</a>
3435
* @see <a href="https://documentation.mailgun.com/en/latest/user_manual.html#sending-in-test-mode">Sending in Test Mode</a>

src/main/java/ru/mystamps/web/support/togglz/Features.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public enum Features implements Feature {
4848
@EnabledByDefault
4949
SEND_ACTIVATION_MAIL,
5050

51+
@Label("Send mail via HTTP API and fallback to SMTP as the last resort")
52+
@EnabledByDefault
53+
SEND_MAIL_VIA_HTTP_API,
54+
5155
@Label("/series/add: show link with auto-suggestions")
5256
@EnabledByDefault
5357
SHOW_SUGGESTION_LINK;

src/main/resources/application-test.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ spring.thymeleaf.prefix: /WEB-INF/views/
3131
spring.thymeleaf.suffix: .html
3232
spring.thymeleaf.cache: false
3333

34+
mailgun.endpoint: http://127.0.0.1:8888/mailgun/send-message
35+
mailgun.password: secret
36+
3437
liquibase.contexts: scheme, init-data, test-data
3538
liquibase.change-log: classpath:/liquibase/changelog.xml
3639

@@ -115,7 +118,6 @@ spring.autoconfigure.exclude: \
115118
, org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration \
116119
, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration \
117120
, org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration \
118-
, org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration \
119121
, org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration \
120122
, org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration \
121123
, org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration \

src/main/resources/application-travis.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ spring.messages.basename: \
2020
ru/mystamps/i18n/SpringSecurityMessages, \
2121
ru/mystamps/i18n/MailTemplates
2222

23+
mailgun.endpoint: http://127.0.0.1:8888/mailgun/send-message
24+
mailgun.password: secret
25+
2326
spring.thymeleaf.mode: HTML
2427
spring.thymeleaf.prefix: /WEB-INF/views/
2528
spring.thymeleaf.suffix: .html
@@ -113,7 +116,6 @@ spring.autoconfigure.exclude: \
113116
, org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration \
114117
, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration \
115118
, org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration \
116-
, org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration \
117119
, org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration \
118120
, org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration \
119121
, org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration \

vagrant/provisioning/roles/mystamps-app/templates/application-prod.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ spring.thymeleaf.prefix: /WEB-INF/views/
2525
spring.thymeleaf.suffix: .html
2626
spring.thymeleaf.cache: true
2727

28+
mailgun.endpoint: https://api.mailgun.net/v3/my-stamps.ru/messages
29+
mailgun.password: {{ mailgun_api_password }}
30+
2831
# see also duplicate definition at pom.xml
2932
liquibase.contexts: scheme, init-data, prod-data
3033
liquibase.change-log: classpath:/liquibase/changelog.xml
@@ -115,7 +118,6 @@ spring.autoconfigure.exclude: \
115118
, org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration \
116119
, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration \
117120
, org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration \
118-
, org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration \
119121
, org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration \
120122
, org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration \
121123
, org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration \

vagrant/provisioning/vagrant.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
profile: 'test'
88
user_db_password: 'q1'
99
user_mail_password: 'q2'
10+
mailgun_api_password: 'q3'
1011
# required for Ubuntu 16.04. which installs Python 2.x to a non-standard path
1112
ansible_python_interpreter: "/usr/bin/python2.7"
1213

0 commit comments

Comments
 (0)