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