En este post realizaremos una prueba integral de conexión del proveedor de correos electrónicos desde una aplicación desarrollada en Java con Spring Boot, empaquetada en una imagen Docker.
El objetivo principal es verificar que el probeedor de correos se conecte correctamente en cada nivel de la infraestructura, desde la aplicación hasta la conectividad con el servidor SMTP, así como identificar y analizar posibles errores que puedan presentarse durante el proceso.
A lo largo del post abordaremos los siguientes puntos:
- Configuración y validación del envío de correos desde la aplicación Spring Boot.
- Verificación del funcionamiento dentro del contenedor Docker.
- Estrategias para testear cada capa (aplicación, contenedor y clúster) y así aislar fallos de configuración, red o credenciales.
- Análisis de errores comunes relacionados con SMTP, autenticación y restricciones de red.
Como proveedor de correo utilizaremos Gmail, lo que nos permitirá revisar consideraciones importantes como autenticación, seguridad y políticas de acceso.
Pasos para crear aplicación Java con docker
Crear aplicacion Java Spring Boot
Procedemos a crear una aplicación de spring boot con las siguientes características:
- Project: maven
- Language: Java
- Spring boot: 3.2.2
- Project Metadata:
- Group: com.example
- Artifact: freemarker
- Name: freemarker
- Description: Ejercicio de freemarker
- Package name: com.example.freemarker
- Packaging: Jar
- Java: 17
- Dependencies
- Spring Web
- Spring Boot DevTools
- Apache Freemarker
Mas informacion sobre aplicaciones spring boot en cómo crear un Entorno de trabajo para Spring boot en eclipse
Procedemos a cargar el proyecto en eclipse o el IDE que prefieras trabajar con spring boot, en el link anterior explico como hacer la carga del proyecto.
Crear proyecto en Java
Procedemos a crear la siguiente estructura de de directorios, paquetes y clases en Java
Crear clase principal
Procedemos a crear la clase controller EmailController.java donde colocaremos el siguiente codigo
package com.example.demo.controller;
import java.util.Properties;
import javax.mail.MessagingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mail.MailSendException;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EmailController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping("/send-email")
public String sendEmail() {
logger.info("sendEmail()");
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("<HOST_DEL_EMAIL>");
mailSender.setUsername("<EMAIL>");
mailSender.setPassword("<PASSWORD>");
mailSender.setPort(Integer.valueOf("465"));
mailSender.setDefaultEncoding("UTF-8");
Properties properties = new Properties();
properties.put("mail.smtp.auth", "true");
properties.put("mail.smtp.connectiontimeout", "10000");
properties.put("mail.smtp.ssl.enable", "true");
properties.put("mail.smtp.ssl.trust", "*");
properties.put("mail.smtp.starttls.enable", "false");
properties.put("mail.smtp.timeout", "10000");
properties.put("mail.imap.ssl.trust", "*");
properties.put("mail.imap.ssl.enable", "true");
properties.put("mail.imap.timeout", "10000");
properties.put("mail.imap.connectiontimeout", "10000");
properties.put("mail.store.protocol", "smtp");
properties.put("mail.debug", "true");
if (properties.size() > 0) {
mailSender.setJavaMailProperties(properties);
}
try {
logger.info("sendEmail(Inicia testConnection)");
mailSender.testConnection();
logger.info("sendEmail(Fin testConnection)");
} catch (MailSendException ex) {
logger.error("MailSendException {}", ex.getMessage());
ex.printStackTrace();
return "Error MailSendException";
} catch (MessagingException e) {
logger.error("MessagingException {}", e.getMessage());
e.printStackTrace();
return "Error MessagingException";
}
logger.info("sendEmail(Fin prueba)");
return "Correo enviado!";
}
}
Que hace el código anterior:
1️⃣ Instancia el cliente SMTP en Java.
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
En lugar de usar application.properties, aquí todo se configura programáticamente.
2️⃣ Credenciales del servidor SMTP:
mailSender.setHost("<HOST_DEL_EMAIL>");
mailSender.setUsername("<EMAIL>");
mailSender.setPassword("<PASSWORD>");
host: servidor SMTP (ej. smtp.gmail.com).username: cuenta de correo.password: contraseña o app password.
3️⃣ Puerto SMTP:
mailSender.setPort(Integer.valueOf("465"));
465: SMTP sobre SSL implícito.- Alternativa común:
587con STARTTLS.
4️⃣ Este método define la codificación de caracteres (ej. UTF-8).
mailSender.setDefaultEncoding("UTF-8");
5️⃣ Inicializamos el objeto properties
Properties properties = new Properties();
Objeto donde se definen parámetros de bajo nivel para JavaMail.
6️⃣ Habilita autenticación SMTP
properties.put("mail.smtp.auth", "true");
7️⃣ Tiempo máximo de espera (ms) para:
properties.put("mail.smtp.connectiontimeout", "10000");
properties.put("mail.smtp.timeout", "10000");
Tiempo máximo de espera (ms) para:
- Conexión
- Respuesta del servidor
Evita bloqueos indefinidos.
8️⃣ Configuramos las propiedades de mail
SSL / TLS
properties.put("mail.smtp.ssl.enable", "true");
Activa SSL (requerido para puerto 465).
Desactiva STARTTLS.
properties.put("mail.smtp.starttls.enable", "false");
- Correcto cuando se usa SSL implícito.
- Si usaras puerto 587, debería ser
true.
Confía en todos los certificados SSL.
properties.put("mail.smtp.ssl.trust", "*");
- Útil para pruebas.
- No recomendado en producción por seguridad.
Propiedades IMAP (no necesarias aquí)
properties.put("mail.imap.ssl.enable", "true");
properties.put("mail.imap.ssl.trust", "*");
properties.put("mail.imap.timeout", "10000");
properties.put("mail.imap.connectiontimeout", "10000");
Activa logs detallados de JavaMail.
properties.put("mail.debug", "true");
Muy útil para:
- Ver handshake SSL
- Errores de autenticación
- Negociación de protocolos
9️⃣ Asignación de propiedades
if (properties.size() > 0) {
mailSender.setJavaMailProperties(properties);
}
- Aplica las propiedades al JavaMailSender.
- La condición es redundante, pero no incorrecta.
🔟 Prueba de conexión SMTP
mailSender.testConnection();
No envía correos, solo:
- Abre conexión
- Autentica
- Cierra sesión
Ideal para:
- Validar credenciales
- Probar red, firewall, SSL
Procedemos a ejecutar, compilar
Tenemos dos opciones, si esta trabajando con eclipse o sts4 puede usar las opciones de compilar y ejecutar del mismo IDE
Nota: tambien puede hacerlo desde la terminal de comandos ejecutado el compilador de maven y luego empaquetar la aplicación:
mvn clean package
Luego ejecutar como una aplicacion java de la siguiente forma
java -jar target/tu-proyecto-1.0.0.jar
Ejecutamos en el navegador o por curl
Para probar que este funcionado la validación del correo, realizamos la ejecución en el navegador accediendo por la url http://localhost:8080/send-email
Tambien podemos hacer el llamado por medio del comando curl.
curl "http://localhost:8080/send-email"
Al revisar los logs que genera Spring Boot, mostrara lo siguiente
DEBUG: Jakarta Mail version 1.6.7
DEBUG: successfully loaded resource: /META-INF/javamail.default.providers
DEBUG: Tables of loaded providers
DEBUG: Providers Listed By Class Name: {com.sun.mail.smtp.SMTPTransport=javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], com.sun.mail.imap.IMAPSSLStore=javax.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], com.sun.mail.pop3.POP3Store=javax.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], com.sun.mail.smtp.SMTPSSLTransport=javax.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], com.sun.mail.imap.IMAPStore=javax.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], com.sun.mail.pop3.POP3SSLStore=javax.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
DEBUG: Providers Listed By Protocol: {imap=javax.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], smtp=javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], pop3=javax.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], imaps=javax.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], smtps=javax.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], pop3s=javax.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
DEBUG: successfully loaded resource: /META-INF/javamail.default.address.map
DEBUG: getProvider() returning javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle]
DEBUG SMTP: useEhlo true, useAuth true
DEBUG SMTP: trying to connect to host "smtp.gmail.com", port 465, isSSL true
220 smtp.gmail.com ESMTP 00721157ae682-79482761800sm2854017b3.4 - gsmtp
DEBUG SMTP: connected to host "smtp.gmail.com", port: 465
EHLO DESKTOP-1801MLQ
250-smtp.gmail.com at your service, [200.118.238.95]
250-SIZE 35882577
250-8BITMIME
250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8
DEBUG SMTP: Found extension "SIZE", arg "35882577"
DEBUG SMTP: Found extension "8BITMIME", arg ""
DEBUG SMTP: Found extension "AUTH", arg "LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH"
DEBUG SMTP: Found extension "ENHANCEDSTATUSCODES", arg ""
DEBUG SMTP: Found extension "PIPELINING", arg ""
DEBUG SMTP: Found extension "CHUNKING", arg ""
DEBUG SMTP: Found extension "SMTPUTF8", arg ""
DEBUG SMTP: protocolConnect login, host=smtp.gmail.com, user=CORREO_ELECTRONICO@gmail.com, password=<non-null>
DEBUG SMTP: Attempt to authenticate using mechanisms: LOGIN PLAIN DIGEST-MD5 NTLM XOAUTH2
DEBUG SMTP: Using mechanism LOGIN
DEBUG SMTP: AUTH LOGIN command trace suppressed
DEBUG SMTP: AUTH LOGIN succeeded
QUIT
221 2.0.0 closing connection 00721157ae682-79482761800sm2854017b3.4 - gsmtp
Crear una imagen Docker
Vamos a crear la siguiente estructura
demo/
├── Dockerfile
├── pom.xml
└── target/
└── demo-0.0.1-SNAPSHOT.jar
Crear archivo Dockerfile
Procedemos a crear el archivo Dockerfile
# ---------- STAGE 1: BUILD ----------
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /build
# Copiamos solo lo necesario para aprovechar cache
COPY pom.xml .
RUN mvn -B -q dependency:go-offline
COPY src ./src
RUN mvn -B -q clean package -DskipTests
# ---------- STAGE 2: RUNTIME ----------
FROM eclipse-temurin:17-jre-alpine
# Crear usuario no root (buena práctica)
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
WORKDIR /app
# Copiamos solo el JAR final
COPY --from=builder /build/target/*.jar app.jar
# Puerto típico Spring Boot
EXPOSE 8080
# JVM optimizada para contenedores
ENTRYPOINT ["java","-XX:+UseContainerSupport","-XX:MaxRAMPercentage=75","-jar","app.jar"]
🔹 Multi-stage build
- Reduce tamaño final de la imagen
- No incluyes Maven ni código fuente en runtime
- Imagen final ≈ 90–120 MB
🔹 Imagen base
eclipse-temurin:17-jre-alpine
✔ OpenJDK oficial
✔ Alpine → liviano
✔ Ideal para Kubernetes y Cloud Run
🔹 Usuario no root
USER spring:spring
- Mejora seguridad
- Evita privilegios innecesarios
🔹 JVM flags
-XX:+UseContainerSupport -XX:MaxRAMPercentage=75
- Permite que la JVM respete los límites del contenedor
- Evita OOM en Kubernetes
Construcción de la imagen
docker build -t springboot-email:1.0 .
Ejecución local
docker run -p 8080:8080 springboot-email:1.0
Prueba: