Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
securis/src/main/java/net/curisit/securis/utils/EmailManager.java
....@@ -1,3 +1,6 @@
1
+/*
2
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
3
+ */
14 package net.curisit.securis.utils;
25
36 import java.io.File;
....@@ -40,22 +43,61 @@
4043 import org.apache.logging.log4j.Logger;
4144
4245 /**
43
- * Component that send emails using Mailgun API:
44
- * http://documentation.mailgun.com/user_manual.html#sending-messages
45
- *
46
- * @author roberto <roberto.sanchez@curisit.net>
46
+ * EmailManager
47
+ * <p>
48
+ * Small utility to send plain-text emails (optionally with one attachment)
49
+ * using the <b>Mailgun</b> API over HTTPS.
50
+ * <p>
51
+ * Design notes:
52
+ * <ul>
53
+ * <li>Reads Mailgun credentials and the "from" address from {@link Config}.</li>
54
+ * <li>Builds a preconfigured {@link HttpClientBuilder} with basic auth and a permissive SSL socket factory.</li>
55
+ * <li>Exposes synchronous (blocking) and asynchronous (non-blocking) send methods.</li>
56
+ * <li>Scope is {@code @ApplicationScoped}; the underlying builder is created once per container.</li>
57
+ * </ul>
58
+ *
59
+ * Thread-safety:
60
+ * <p>
61
+ * The class is effectively stateless after construction; using a shared {@link HttpClientBuilder}
62
+ * is safe as a new {@link HttpClient} is built per request.
63
+ *
64
+ * Configuration keys (see {@link Config.KEYS}):
65
+ * <ul>
66
+ * <li>{@code mailgun.domain}</li>
67
+ * <li>{@code mailgun.api.key}</li>
68
+ * <li>{@code email.from.address}</li>
69
+ * </ul>
70
+ *
71
+ * Failure handling:
72
+ * <p>
73
+ * Network and HTTP errors are surfaced as {@link SeCurisServiceException} with appropriate error codes.
74
+ *
75
+ * @author roberto &lt;roberto.sanchez@curisit.net&gt;
76
+ * Last reviewed by JRA on Oct 6, 2025.
4777 */
4878 @ApplicationScoped
4979 public class EmailManager {
5080
81
+ /** Class logger. */
5182 private static final Logger LOG = LogManager.getLogger(EmailManager.class);
5283
84
+ /** Mailgun endpoint composed from configured domain. */
5385 private final String serverUrl;
86
+
87
+ /** Preconfigured builder that carries SSL and basic-auth configuration. */
5488 private final HttpClientBuilder httpClientBuilder;
5589
90
+ // ---------------------------------------------------------------------
91
+ // Constructors
92
+ // ---------------------------------------------------------------------
93
+
5694 /**
57
- *
58
- * @throws SeCurisException
95
+ * EmailManager
96
+ * <p>
97
+ * Default constructor that validates required configuration and prepares an
98
+ * HTTP client builder with Mailgun credentials and SSL settings.
99
+ *
100
+ * @throws SeCurisException if mandatory configuration is missing or the SSL builder cannot be created
59101 */
60102 public EmailManager() throws SeCurisException {
61103 String domain = Config.get(Config.KEYS.MAILGUN_DOMAIN);
....@@ -64,13 +106,29 @@
64106 }
65107 serverUrl = String.format("https://api.mailgun.net/v2/%s/messages", domain);
66108 httpClientBuilder = createHttpClient();
67
-
68109 }
69110
111
+ // ---------------------------------------------------------------------
112
+ // Internal helpers
113
+ // ---------------------------------------------------------------------
114
+
115
+ /**
116
+ * createHttpClient
117
+ * <p>
118
+ * Builds a {@link HttpClientBuilder} that:
119
+ * <ul>
120
+ * <li>Accepts any server certificate (permissive trust strategy).</li>
121
+ * <li>Applies HTTP Basic Auth using Mailgun's API key as the password and user "api".</li>
122
+ * </ul>
123
+ *
124
+ * @return a preconfigured {@link HttpClientBuilder} ready to build clients
125
+ * @throws SeCurisException if SSL initialization fails
126
+ */
70127 private HttpClientBuilder createHttpClient() throws SeCurisException {
71128 SSLContextBuilder builder = new SSLContextBuilder();
72129 SSLConnectionSocketFactory sslsf = null;
73130 try {
131
+ // Trust all X509 certificates (relies on HTTPS + Basic Auth; consider hardening in production).
74132 builder.loadTrustMaterial((KeyStore) null, new TrustStrategy() {
75133 @Override
76134 public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
....@@ -82,30 +140,45 @@
82140 LOG.error(e1);
83141 throw new SeCurisException("Error creating SSL socket factory");
84142 }
143
+
144
+ // Configure Basic Auth with Mailgun API key
85145 CredentialsProvider provider = new BasicCredentialsProvider();
86
- UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("api", Config.get(Config.KEYS.MAILGUN_API_KEY));
146
+ UsernamePasswordCredentials credentials =
147
+ new UsernamePasswordCredentials("api", Config.get(Config.KEYS.MAILGUN_API_KEY));
87148 provider.setCredentials(AuthScope.ANY, credentials);
88149
89
- return HttpClientBuilder.create().setDefaultCredentialsProvider(provider).setSSLSocketFactory(sslsf);
150
+ return HttpClientBuilder.create()
151
+ .setDefaultCredentialsProvider(provider)
152
+ .setSSLSocketFactory(sslsf);
90153 }
91154
155
+ // ---------------------------------------------------------------------
156
+ // Email sending API
157
+ // ---------------------------------------------------------------------
158
+
92159 /**
93
- * Basic method to send emails in text mode with attachment. The method is
94
- * synchronous, It waits until server responses.
95
- *
96
- * @param subject
97
- * @param body
98
- * @param to
99
- * @param file
100
- * @throws SeCurisException
101
- * @throws UnsupportedEncodingException
160
+ * sendEmail
161
+ * <p>
162
+ * Sends a plain-text email (UTF-8) via Mailgun. Optionally attaches a single file.
163
+ * This call is <b>synchronous</b> (blocking) and only returns once the HTTP response is received.
164
+ *
165
+ * @param subject Email subject (will be sent as UTF-8).
166
+ * @param body Email body in plain text (UTF-8).
167
+ * @param to Recipient address (required).
168
+ * @param cc Optional CC address, may be {@code null}.
169
+ * @param file Optional file to attach, may be {@code null}.
170
+ *
171
+ * @throws SeCurisServiceException if the HTTP call fails or Mailgun responds with a non-200 status
172
+ * @throws UnsupportedEncodingException kept for API compatibility (body/subject are forced to UTF-8)
102173 */
103174 @SuppressWarnings("deprecation")
104
- public void sendEmail(String subject, String body, String to, String cc, File file) throws SeCurisServiceException, UnsupportedEncodingException {
175
+ public void sendEmail(String subject, String body, String to, String cc, File file)
176
+ throws SeCurisServiceException, UnsupportedEncodingException {
177
+
105178 HttpPost postRequest = new HttpPost(serverUrl);
106179
180
+ // Build multipart form body compatible with Mailgun
107181 MultipartEntityBuilder builder = MultipartEntityBuilder.create();
108
-
109182 builder.setCharset(Charset.forName("utf-8"));
110183 builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
111184 builder.addTextBody("from", Config.get(Config.KEYS.EMAIL_FROM_ADDRESS));
....@@ -113,28 +186,34 @@
113186 if (cc != null) {
114187 builder.addTextBody("cc", cc);
115188 }
116
- builder.addTextBody("subject", subject, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
117
- builder.addTextBody("text", body, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
118
- if (file != null) {
189
+ builder.addTextBody("subject",
190
+ subject, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
191
+ builder.addTextBody("text",
192
+ body, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
119193
194
+ if (file != null) {
120195 LOG.info("File to attach: {}", file.getAbsoluteFile());
121196 builder.addPart("attachment", new FileBody(file));
122197 }
123198
124199 postRequest.setEntity(builder.build());
200
+
201
+ // Execute HTTP request
125202 HttpResponse response;
126203 HttpClient httpClient = httpClientBuilder.build();
127204 try {
128205 response = httpClient.execute(postRequest);
129206
207
+ // Mailgun returns JSON. We parse it to a Map for logging/validation.
130208 String jsonLic = IOUtils.toString(response.getEntity().getContent());
131209 if (response.getStatusLine().getStatusCode() == 200) {
132210 LOG.debug("Response content read OK: {}", jsonLic);
133211 Map<String, Object> responseBean = JsonUtils.json2map(jsonLic);
134
-
135212 LOG.debug("Response mail read OK: {}", responseBean);
136213 } else {
137
- throw new SeCurisServiceException(ErrorCodes.UNEXPECTED_ERROR, "Error sending email, response estatus: " + response.getStatusLine());
214
+ throw new SeCurisServiceException(
215
+ ErrorCodes.UNEXPECTED_ERROR,
216
+ "Error sending email, response estatus: " + response.getStatusLine());
138217 }
139218 } catch (IOException e) {
140219 LOG.error("Error sending email", e);
....@@ -143,21 +222,27 @@
143222 }
144223
145224 /**
146
- * Basic method to send emails in text mode with attachment. The method is
147
- * asynchronous, It returns immediately
148
- *
149
- * @param subject
150
- * @param body
151
- * @param to
152
- * @param file
153
- * @throws SeCurisException
154
- * @throws UnsupportedEncodingException
225
+ * sendEmailAsync
226
+ * <p>
227
+ * Asynchronous variant of {@link #sendEmail(String, String, String, String, File)}.
228
+ * The call returns immediately and performs the HTTP request in a single-thread executor.
229
+ *
230
+ * @param subject Email subject (UTF-8).
231
+ * @param body Email body in plain text (UTF-8).
232
+ * @param to Recipient address.
233
+ * @param cc Optional CC address, may be {@code null}.
234
+ * @param file Optional attachment, may be {@code null}.
235
+ * @param callback Non-null callback to be notified on success or failure.
236
+ *
237
+ * @throws SeCurisException if there is a configuration or environment problem before dispatch
238
+ * @throws UnsupportedEncodingException for API compatibility (subject/body are encoded as UTF-8)
155239 */
156
- public void sendEmailAsync(String subject, String body, String to, String cc, File file, EmailCallback callback) throws SeCurisException,
157
- UnsupportedEncodingException {
240
+ public void sendEmailAsync(
241
+ String subject, String body, String to, String cc, File file, EmailCallback callback)
242
+ throws SeCurisException, UnsupportedEncodingException {
243
+
158244 Executor ex = Executors.newSingleThreadExecutor();
159245 ex.execute(new Runnable() {
160
-
161246 @Override
162247 public void run() {
163248 try {
....@@ -168,37 +253,61 @@
168253 } catch (SeCurisServiceException e) {
169254 callback.error(e);
170255 }
171
-
172256 }
173257 });
174
-
175258 }
176259
260
+ // ---------------------------------------------------------------------
261
+ // Callback contract
262
+ // ---------------------------------------------------------------------
263
+
264
+ /**
265
+ * EmailCallback
266
+ * <p>
267
+ * Functional contract to be notified when an async send finishes.
268
+ */
177269 public static interface EmailCallback {
270
+ /**
271
+ * success<p>
272
+ * Called when the email was sent and Mailgun returned HTTP 200.
273
+ */
178274 public void success();
179275
276
+ /**
277
+ * error<p>
278
+ * Called when there was a problem sending the email.
279
+ *
280
+ * @param e encapsulates the reason of failure
281
+ */
180282 public void error(SeCurisServiceException e);
181283 }
182284
285
+ // ---------------------------------------------------------------------
286
+ // Manual test harness
287
+ // ---------------------------------------------------------------------
288
+
289
+ /**
290
+ * main<p>
291
+ * Simple manual test for the async email flow. Adjust addresses and file path before use.
292
+ *
293
+ * @param args program arguments (unused)
294
+ * @throws SeCurisException if configuration is invalid
295
+ * @throws UnsupportedEncodingException if UTF-8 encoding fails (unlikely)
296
+ */
183297 public static void main(String[] args) throws SeCurisException, UnsupportedEncodingException {
184
- // new EmailManager().sendEmail("España así de bien",
185
- // "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response",
186
- // "info@r75.es", new File(
187
- // "/Users/rob/Downloads/test.req"));
188
- new EmailManager().sendEmailAsync("España así de bien", "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response", "info@r75.es",
189
- "dev@r75.es", new File("/Users/rob/Downloads/test.req"), new EmailCallback() {
298
+ // Example async call (subject/body contain non-ASCII content to validate UTF-8 handling).
299
+ new EmailManager().sendEmailAsync("España así de bien",
300
+ "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response",
301
+ "info@r75.es",
302
+ "dev@r75.es",
303
+ new File("/Users/rob/Downloads/test.req"),
304
+ new EmailCallback() {
305
+ @Override
306
+ public void success() { LOG.info("Success!!!"); }
190307
191308 @Override
192
- public void success() {
193
- LOG.info("Success!!!");
194
- }
195
-
196
- @Override
197
- public void error(SeCurisServiceException e) {
198
- LOG.error("Error: {} !!!", e);
199
- }
309
+ public void error(SeCurisServiceException e) { LOG.error("Error: {} !!!", e); }
200310 });
201311 LOG.info("Waiting for email to be sent...");
202312 }
203
-
204313 }