Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/*
 * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
 */
package net.curisit.securis.utils;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import jakarta.enterprise.context.ApplicationScoped;
import net.curisit.securis.SeCurisException;
import net.curisit.securis.services.exception.SeCurisServiceException;
import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
 * EmailManager
 * <p>
 * Small utility to send plain-text emails (optionally with one attachment)
 * using the <b>Mailgun</b> API over HTTPS.
 * <p>
 * Design notes:
 * <ul>
 *   <li>Reads Mailgun credentials and the "from" address from {@link Config}.</li>
 *   <li>Builds a preconfigured {@link HttpClientBuilder} with basic auth and a permissive SSL socket factory.</li>
 *   <li>Exposes synchronous (blocking) and asynchronous (non-blocking) send methods.</li>
 *   <li>Scope is {@code @ApplicationScoped}; the underlying builder is created once per container.</li>
 * </ul>
 *
 * Thread-safety:
 * <p>
 * The class is effectively stateless after construction; using a shared {@link HttpClientBuilder}
 * is safe as a new {@link HttpClient} is built per request.
 *
 * Configuration keys (see {@link Config.KEYS}):
 * <ul>
 *   <li>{@code mailgun.domain}</li>
 *   <li>{@code mailgun.api.key}</li>
 *   <li>{@code email.from.address}</li>
 * </ul>
 *
 * Failure handling:
 * <p>
 * Network and HTTP errors are surfaced as {@link SeCurisServiceException} with appropriate error codes.
 *
 * @author roberto &lt;roberto.sanchez@curisit.net&gt;
 * Last reviewed by JRA on Oct 6, 2025.
 */
@ApplicationScoped
public class EmailManager {
    /** Class logger. */
    private static final Logger LOG = LogManager.getLogger(EmailManager.class);
    /** Mailgun endpoint composed from configured domain. */
    private final String serverUrl;
    /** Preconfigured builder that carries SSL and basic-auth configuration. */
    private final HttpClientBuilder httpClientBuilder;
    // ---------------------------------------------------------------------
    // Constructors
    // ---------------------------------------------------------------------
    /**
     * EmailManager
     * <p>
     * Default constructor that validates required configuration and prepares an
     * HTTP client builder with Mailgun credentials and SSL settings.
     *
     * @throws SeCurisException if mandatory configuration is missing or the SSL builder cannot be created
     */
    public EmailManager() throws SeCurisException {
        String domain = Config.get(Config.KEYS.MAILGUN_DOMAIN);
        if (domain == null) {
            throw new SeCurisException("Please, add '" + Config.KEYS.MAILGUN_DOMAIN + "' parameter to config file");
        }
        serverUrl = String.format("https://api.mailgun.net/v2/%s/messages", domain);
        httpClientBuilder = createHttpClient();
    }
    // ---------------------------------------------------------------------
    // Internal helpers
    // ---------------------------------------------------------------------
    /**
     * createHttpClient
     * <p>
     * Builds a {@link HttpClientBuilder} that:
     * <ul>
     *   <li>Accepts any server certificate (permissive trust strategy).</li>
     *   <li>Applies HTTP Basic Auth using Mailgun's API key as the password and user "api".</li>
     * </ul>
     *
     * @return a preconfigured {@link HttpClientBuilder} ready to build clients
     * @throws SeCurisException if SSL initialization fails
     */
    private HttpClientBuilder createHttpClient() throws SeCurisException {
        SSLContextBuilder builder = new SSLContextBuilder();
        SSLConnectionSocketFactory sslsf = null;
        try {
            // Trust all X509 certificates (relies on HTTPS + Basic Auth; consider hardening in production).
            builder.loadTrustMaterial((KeyStore) null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    return true;
                }
            });
            sslsf = new SSLConnectionSocketFactory(builder.build());
        } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e1) {
            LOG.error(e1);
            throw new SeCurisException("Error creating SSL socket factory");
        }
        // Configure Basic Auth with Mailgun API key
        CredentialsProvider provider = new BasicCredentialsProvider();
        UsernamePasswordCredentials credentials =
            new UsernamePasswordCredentials("api", Config.get(Config.KEYS.MAILGUN_API_KEY));
        provider.setCredentials(AuthScope.ANY, credentials);
        return HttpClientBuilder.create()
                .setDefaultCredentialsProvider(provider)
                .setSSLSocketFactory(sslsf);
    }
    // ---------------------------------------------------------------------
    // Email sending API
    // ---------------------------------------------------------------------
    /**
     * sendEmail
     * <p>
     * Sends a plain-text email (UTF-8) via Mailgun. Optionally attaches a single file.
     * This call is <b>synchronous</b> (blocking) and only returns once the HTTP response is received.
     *
     * @param subject Email subject (will be sent as UTF-8).
     * @param body    Email body in plain text (UTF-8).
     * @param to      Recipient address (required).
     * @param cc      Optional CC address, may be {@code null}.
     * @param file    Optional file to attach, may be {@code null}.
     *
     * @throws SeCurisServiceException   if the HTTP call fails or Mailgun responds with a non-200 status
     * @throws UnsupportedEncodingException kept for API compatibility (body/subject are forced to UTF-8)
     */
    @SuppressWarnings("deprecation")
    public void sendEmail(String subject, String body, String to, String cc, File file)
            throws SeCurisServiceException, UnsupportedEncodingException {
        HttpPost postRequest = new HttpPost(serverUrl);
        // Build multipart form body compatible with Mailgun
        MultipartEntityBuilder builder = MultipartEntityBuilder.create();
        builder.setCharset(Charset.forName("utf-8"));
        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        builder.addTextBody("from", Config.get(Config.KEYS.EMAIL_FROM_ADDRESS));
        builder.addTextBody("to", to);
        if (cc != null) {
            builder.addTextBody("cc", cc);
        }
        builder.addTextBody("subject",
                subject, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
        builder.addTextBody("text",
                body, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
        if (file != null) {
            LOG.info("File to attach: {}", file.getAbsoluteFile());
            builder.addPart("attachment", new FileBody(file));
        }
        postRequest.setEntity(builder.build());
        // Execute HTTP request
        HttpResponse response;
        HttpClient httpClient = httpClientBuilder.build();
        try {
            response = httpClient.execute(postRequest);
            // Mailgun returns JSON. We parse it to a Map for logging/validation.
            String jsonLic = IOUtils.toString(response.getEntity().getContent());
            if (response.getStatusLine().getStatusCode() == 200) {
                LOG.debug("Response content read OK: {}", jsonLic);
                Map<String, Object> responseBean = JsonUtils.json2map(jsonLic);
                LOG.debug("Response mail read OK: {}", responseBean);
            } else {
                throw new SeCurisServiceException(
                        ErrorCodes.UNEXPECTED_ERROR,
                        "Error sending email, response estatus: " + response.getStatusLine());
            }
        } catch (IOException e) {
            LOG.error("Error sending email", e);
            throw new SeCurisServiceException(ErrorCodes.UNEXPECTED_ERROR, "Error sending email");
        }
    }
    /**
     * sendEmailAsync
     * <p>
     * Asynchronous variant of {@link #sendEmail(String, String, String, String, File)}.
     * The call returns immediately and performs the HTTP request in a single-thread executor.
     *
     * @param subject  Email subject (UTF-8).
     * @param body     Email body in plain text (UTF-8).
     * @param to       Recipient address.
     * @param cc       Optional CC address, may be {@code null}.
     * @param file     Optional attachment, may be {@code null}.
     * @param callback Non-null callback to be notified on success or failure.
     *
     * @throws SeCurisException              if there is a configuration or environment problem before dispatch
     * @throws UnsupportedEncodingException  for API compatibility (subject/body are encoded as UTF-8)
     */
    public void sendEmailAsync(
            String subject, String body, String to, String cc, File file, EmailCallback callback)
            throws SeCurisException, UnsupportedEncodingException {
        Executor ex = Executors.newSingleThreadExecutor();
        ex.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    EmailManager.this.sendEmail(subject, body, to, cc, file);
                    callback.success();
                } catch (UnsupportedEncodingException e) {
                    callback.error(new SeCurisServiceException("Error sending email: " + e));
                } catch (SeCurisServiceException e) {
                    callback.error(e);
                }
            }
        });
    }
    // ---------------------------------------------------------------------
    // Callback contract
    // ---------------------------------------------------------------------
    /**
     * EmailCallback
     * <p>
     * Functional contract to be notified when an async send finishes.
     */
    public static interface EmailCallback {
        /**
         * success<p>
         * Called when the email was sent and Mailgun returned HTTP 200.
         */
        public void success();
        /**
         * error<p>
         * Called when there was a problem sending the email.
         *
         * @param e encapsulates the reason of failure
         */
        public void error(SeCurisServiceException e);
    }
    // ---------------------------------------------------------------------
    // Manual test harness
    // ---------------------------------------------------------------------
    /**
     * main<p>
     * Simple manual test for the async email flow. Adjust addresses and file path before use.
     *
     * @param args program arguments (unused)
     * @throws SeCurisException             if configuration is invalid
     * @throws UnsupportedEncodingException if UTF-8 encoding fails (unlikely)
     */
    public static void main(String[] args) throws SeCurisException, UnsupportedEncodingException {
        // Example async call (subject/body contain non-ASCII content to validate UTF-8 handling).
        new EmailManager().sendEmailAsync("España así de bien",
                "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response",
                "info@r75.es",
                "dev@r75.es",
                new File("/Users/rob/Downloads/test.req"),
                new EmailCallback() {
                    @Override
                    public void success() { LOG.info("Success!!!"); }
                    @Override
                    public void error(SeCurisServiceException e) { LOG.error("Error: {} !!!", e); }
                });
        LOG.info("Waiting for email to be sent...");
    }
}