/* * 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 *

* Small utility to send plain-text emails (optionally with one attachment) * using the Mailgun API over HTTPS. *

* Design notes: *

* * Thread-safety: *

* 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}): *

* * Failure handling: *

* Network and HTTP errors are surfaced as {@link SeCurisServiceException} with appropriate error codes. * * @author roberto <roberto.sanchez@curisit.net> * 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 *

* 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 *

* Builds a {@link HttpClientBuilder} that: *

* * @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 *

* Sends a plain-text email (UTF-8) via Mailgun. Optionally attaches a single file. * This call is synchronous (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 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 *

* 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 *

* Functional contract to be notified when an async send finishes. */ public static interface EmailCallback { /** * success

* Called when the email was sent and Mailgun returned HTTP 200. */ public void success(); /** * error

* 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

* 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..."); } }