Envio de SMS con SNS y EMAIL con AWS en tomcat


 La necesidad de correo de mensajes de Texto a SMS como se conoce es una necesidad casi que obligatoria en el uso de comercio , ventas,  servicios, mantenimiento etc.

Usualmente he venido haciendo uso de las diferentes opciones de mercado pero eso me estaba llevando a que tenga sin numero de opciones dependiendo de los diferentes desarrollos en que participo lo propio para el envío de correos.

Realmente las integraciones suelen ser generalmente sencillas casi nunca mas allá de  configurar un API y eso es todo.

Por esa razón nunca me había preguntada por las opciones de envió de correo y SMS mediante la plataforma de Amazon, mas que todo como muchos sabrán el tema con Amazon a pesar de lo que digan, casi  nunca es trivial , requiere esfuerzo y la curva de aprendizaje suele ser bastante pendiente además de que la documentación suele estar muy dispersa  y incluso des actualizada en muchos de los servicios que prestan.

No es con el animo de criticar sino simplemente de indicar porque casi nunca uso los servicios propios de AWS, sino de terceros los cuales conozco a encuentro información muy accesible.

Sin embargo por una o 2 tres cosas que ya me han venido pasando con los dichos servicios y el soporte de los mismos que en diversas ocasiones he comentado en este mismo blog, como es el caso de los certificados SSL, algunas bases de datos, modelos de encriptamiento y otras varias , de alguna manera empece a buscar las alternativas  en AWS, como digo no siempre es sencillo por no decir nunca , pero de alguna manera la cosa como en todo apenas se va conociendo pues va aflojando.

Para un nuevo proyecto que inicie,  como siempre a la mano había que tener HttpClient , E-Mail y claro SMS.

Dado que había que empezar el proyecto casi de cero por las condiciones del cliente, pues no había demasiado ventaja en usar lo que se tenia ademas que se quería que en lo posible se usaran los recursos del mismo proveedor de servicios para no llegar el tema como tanto he comentado, "Eso no me toca a mi pregúntele a ..." y nadie da respuesta porque siempre la culpa de todo es del otro y con el famoso tema de los Call Center que nunca resuelven nada de fondo pues ni hablar.

Puesto ya a la tarea, pues configure el HttpClient , con las librerías actualizadas de Tomcat sin mayor dificultar en realidad, lo propio de E-Mail , para lo cual use las librerías de Tomcat mail ya que las mismas paginas de AWS recomiendan usarlo con algún lenguajes que tenga el modelo de envío de correos SMTP y como era simplemente pasar algunas variables pues me decidí por usar el que vengo usando desde siempre mail.jar para Tomcat.

 Salvo algunos datos brindados desde AWS, como el puerto que ya nos el 25 sino 587, obtener claro las credenciales desde la consola SES para envío de correos SMTP,  y la definición de HOST = "email-smtp.us-east-1.amazonaws.com" o algo así dependiendo de la región en este caso N. Virginia , y la variable  FROM = "contactenos@www.misitio.com" que se obtiene de la misma consola de SES (send  email simple), en lugar del clásico correo del remitente, algunos pequeños inconvenientes pero en general la literatura que encontré en las mismas paginas de AWS sirvieron al propósito sin muchos inconvenientes y en cosa de 1 0 2 días estaba todo sobre ruedas.

La siguiente clase permite el envío de correo desde Tomcat con uso de librerías javax.mail convencionales, no utilice las librerías de AWS, porque con las venia usando  me funcionó  sin problemas de otros desarrollos, fue solamente colocar los parámetros que proporciona AWS.

---------------------------------------------------------------------------------------------------------------

package email.correo ;


import java.util.* ;
import java.text.* ;
import java.io.* ;
import java.net.* ;
import javax.mail.Authenticator ;
import javax.mail.* ;
import javax.mail.internet.* ;
import java.util.Date ;


/**

@author Carlos Arturo Castaño G.
@version 3.0 04/08/2022

*/

public class SendMail{

    static public String Send(String gSubject,   String gInfo,    String gEmpresaEmail,
                                            String gEmail
)

{
if (Sos.Len(gInfo) <= 0 )

{
     return ("error, No envia correo sin mensaje") ;
}

//FROM es la entidad generada y verificada por SES; https://us-east-1.console.aws.amazon.com/ses
//SMTP USERNAME y SMMTP_PASSWORD son generados en AWS al crear credenciales
//HOST es lo aws denomina las regiones en este caso es US East (N. Virginia) = us-east-1
//PORT 587 es el puerto asignado por aws en lugar del puerto 25 tradicional

String FROM = "contactenos@www.miservidor.com" ;
String FROMNAME = "miservidor.com";
String SMTP_USERNAME = "AKI4R5M4ARQ437M32G35VY";
String SMTP_PASSWORD = "BK0ZyqkmjgXUDa4CDrJyIk0TzHaJifyi7PoVrlQzEu";
String HOST = "email-smtp.us-east-1.amazonaws.com" ;
int PORT = 587 ;

boolean sessionDebug = false ;// true Envie un tracing de la conexión a consola para revisión


// En los siguientes paso no se hace nada especial es que simplemente yo por practica indico con
// mayúsculas que es un parámetro que viene de otra parte y es exterior a mis sistemas y con
// minúsculas son ya manipuladas en mi aplicación, caprichos de uno !       

String host            = HOST ;
String from           = FROM ;
String fromName = FROMNAME ;
int port                 = PORT ;
String user           = SMTP_USERNAME ;
String password  = SMTP_PASSWORD ;
String to              = Sos.Lower(Sos.Trim(gEmail)) ;
String subject      = gSubject ;

String[] bcc = to.split(",") ;
Properties props = System.getProperties() ;
props.put("mail.transport.protocol", "smtp") ;
props.put("mail.host", host) ;
props.put("mail.smtp.port", port) ;
props.put("mail.smtp.auth", "true") ;
props.put("mail.smtp.starttls.enable","true") ;
props.put("mail.smtp.EnableSSL.enable","true") ;
     Session mailSession = Session.getDefaultInstance(props, null)  ;

     mailSession.setDebug(sessionDebug)                                 ;

     String host            = HOST                                                  ;
     String from           = FROM                                                 ;
     String fromName = FROMNAME                                      ;
     int    port              = PORT                                                   ;
     String user           = SMTP_USERNAME                           ;
     String password   = SMTP_PASSWORD                           ;
     String to               = Sos.Lower(Sos.Trim(gEmail))            ;
     String subject       = gSubject                                              ;

     String[] bcc = to.split(",")                                                   ;

     Properties props = System.getProperties()                         ;

     props.put("mail.transport.protocol", "smtp")                     ;
     props.put("mail.host", host)                                               ;
     props.put("mail.smtp.port", port)                                       ;
     props.put("mail.smtp.auth", "true")                                   ;
     props.put("mail.smtp.starttls.enable","true")                     ;
     props.put("mail.smtp.EnableSSL.enable","true")              ;

     Session mailSession = Session.getDefaultInstance(props, null)  ;
     mailSession.setDebug(sessionDebug)                                ;

   try
     {
       Message msg = new MimeMessage(mailSession)           ;
       msg.setFrom(new InternetAddress(from))                       ;

       InternetAddress[] address = new InternetAddress[bcc.length]  ;

       for ( int i = 0; i < bcc.length; i++ )
          {
           address[i] = new InternetAddress(bcc[i])                     ;
           msg.addRecipient(Message.RecipientType.TO, address[i])   ;
          }

       msg.setSentDate(new Date())                                                   ;
       msg.setSubject(subject)                                                           ;
       msg.setContent(gInfo,"text/html")                                          ;


       Transport transport = mailSession.getTransport("smtp")        ;
       transport.connect(host,user,password)                                    ;
       transport.sendMessage(msg, msg.getAllRecipients())           ;
       transport.close();
       return ("true")                                                                         ;
     }
   catch (Exception e)
     {
       System.out.println("Error envio de Email : " + e.toString()) ;
       e.printStackTrace(System.out)                                               ;

       return ("error," + e.toString())                                                ;
      }
  }

}
--------------------------------------------------------------------------------------------------------------

El error 501 error RCPT es muy común es el envió de mensaje de correo , usualmente es una mala

conformación de la dirección de correo a la cual se desea enviar el mensaje,  ente los mas comunes : falta el símbolo @, hay una coma en lugar de un punto.

                                                           ---------- SMS ----------------

Por otra parte de acuerdo  a mi diferentes experiencia de SMS, (mensajes de texto) pensé , para ese 1 0 2 horas y estoy listo.

Pues no señor, nada que ver,  me gaste mas de 10 días y no logré que con las indicaciones ,  de la gente Amazon, muchas lecturas de cuanta entrada a blog había, amén de foros sobre el tema SNS que es el sistema de notificaciones "sencilla" de amazon y que entre otras varias cosas sirve para el envío de SMS logré que me funcionara, y muchas cosas mas.

En resumen con las librerías propias de SNS para SMS no me funcionó de ninguna manera y lo peor en ningún de los muchos que monte conseguí que por lo menos me mostrara un error, simplemente compilaba en Java (8 y 11) todo bien, ejecutaba sin error , pero no envía el mensaje.

Después de perder días con el tema y nada que conseguía un infeliz mensajito de "hola mundo", pensé en una alternativa que en ocasiones me ha a funcionado muy bien para otros desarrollos dentro de java y que consiste es usar un proceso Java que ejecuta en segundo plano comandos del sistema operativo  y devuelve un respuesta, 

El proceso Runtime.getRuntime(), que es sin duda una gran utilidad para usar como navaja suiza para cosas sencillas, algunos se quejan del tema de seguridad y todo eso, y claro que si, pero como digo es para cosas sencillas que no comprometan la seguridad del sistema.

Así las cosas y dejando esto claro me puse a verificar como ponerlo a funcionar.

1. Instale aws-cli : es una buena herramienta de consola para verificar en caliente algunos comandos de AWS, y el cambio y uso de algunos comando , en EC2 es tan simple como usar yum install aws* o dnf install aws* ,  en las ultimas versiones de EC2 ya viene instalado.

2.Desde luego como todo en AWS, debe tener un usuario y contraseña valida, para el caso de SNS claro no es la excepción . recomiendan conseguir un usuario y contraseña de la consola de políticas de seguridad IAM.

Seeleccionar en buscador de Console: IAM

1. Crear usuarios (personas) : Dar un nombre por ejemplo usuarioSNS

2. Seleccionar el nombre del usuario que acaba de crear

3. Crear Clave de acceso ( usualmente esta en la parte superior derecha, crea un clave y clave secreta que guarda en un archivo tipo excel.)

4. Agregar permisos: En la pestaña que se despliega seleccionar nuevamente Agregar permisos

5. Seleccionar: Adjuntar politicas directamente.

Se depliega un menú de búsqueda ingresar SNS y despliega las opciones de las politicas SNS

Seleccionar AmazonSNSReadOnlyAcceso dando click en el recuadro izquierdo a la etiqueda deplegada 

Ir a la parte inferior de la pantalla , esta el botón siguiente el cual crea eusuario IAM

Tener a mano a el usuario y contraseña entregada por el generador IAM.

3. Ingresar a la consola SNS de AWS y configurar un Tema (tópico) puede hacerlo tomando todo por defecto, en realidad para enviar mensajes a números telefónicos no se usa, pero debe registrarse para poder crear un perfil y solicitar que te den permisos para el envió de mensajes SMS fuera del ambiente de prueba , ellos lo llaman sandox.

O sea modo de producción, para ese proceso te piden que envié un correo justificando el uso de los SMS para proyectos, en realidad un formulario con 3 o 4 peguntas muy simples bajo una pestaña que dice algo como  "salir de sandbox" o " ambiente aislado" o algo similar, algo que te indique que quieres trabajar sin restricciones.

Los pasos 2 y 3 son generales independientes de la forma que quieras usar el SNS para el envió de mensajes ya sea por la consola de AWS, por aws-cli o por un lenguaje de desarrollo siempre tienes que tener los permisos IAM y configuración SNS.

Ahora con un editor de texto cualquiera como edit o emacs , colocas la carpeta .aws en el /root y dentro de ella el archivo  queda algo así:

/root/.aws/credentials
que debe contener el usuario y contraseña que te entrego IAM
debe ver algo similar a esto

/root/.aws/credentials
--------------------------------------------------------------------------------
[default]
aws_access_key_id = AK14R5M14R0A7DBX8VUA
aws_secret_access_key = flyn8CAC999zOiLAvan1EDJ281thSOSWB2GROvalJKN0
--------------------------------------------------------------------------------

Y crear el archivo denominado config en la misma carpeta, se debe así

/root/.aws/config
--------------------------------------------------------------------------------
[default]
region = us-east-1

La región depende la donde este ubicado su servidor en este caso en N. Virgina que tiene por código us-east-1, es muy importante  definir estos archivos y colocar allí los valores correctos, pues aunque dicen en todas partes que las variables de entorno,

export AWS_ACCESS_KEY_ID='AK14R5M14R0A7DBX8VUA'
export AWS_SECRET_KEY='flyn8CAC999zOiLAvan1EDJ281thSOSWB2GROvalJKN0'
export AWS_DEFAULT_REGION=us-east-1

Deben funcionar, pero yo no logre que en mi sistema las procesara aunque una simple consulta al sistema operativo con #sudo export -p , decía que si que hay estaba , pues no me funcionaba.

Si todo quedo correcto un simple ensayo del comando aws-cli debería permitir enviar un SMS

# sudo -i

Para ingresar a modo root
En la consola aws-cli , digitar el comando con sus parámetros.

aws sns publish --phone-number +5731800000 --subject "ejemplo" --message "Hola mundo"

Si tiene todo bien debe ver el menaje en su celular Hola Mundo, observe que el numero de el celular a donde esta enviando el mensaje esta precedido por el signo (+) y el indicativo de su país en este caso 57 para Colombia.

Vamos entonces a crear un script bajo /usr/bin/sms.sh que haga esto mismo pero ya con parámetros.
como vamos a usar un comando del shell es necesario hacer un pequeño truco para que el shell no
interprete cada palabra separada con un espacio como una nueva variable, en este caso uso el guión bajo como separador así :"Hola_Mundo" y en script sms.sh los retiro para que quede la cadena original 

/usr/bin/sms.sh
-----------------------------------------------------------------------------------------------------------------------  
#Leo las variables como vienen desde Tomcat. (mas abajo veremos la clase para enviarlos)

eval telefono="$1"
eval subject="$2"
eval info="$3"

# Regresa los mensajes a su forma original con guiones bajo, a espacios en blanco.

subject="$(echo $subject | tr "_" " ")"
info="$(echo $info | tr "_" " ")"

# Exporta las variables de entorno necesarias para que encuentre las credenciales. Los pasa al
# entorno sudo bash que vamos a usar para invocar el comando aws sns de AWS que envía el
# mensaje. Repito yo no pude hacer funcionar las variables de entorno que tanto recomiendan
# pero con este sencillo script toma las variables de forma correcta desde los archivos a que apunto.
# y que previamente configure.
 
export AWS_CONFIG_FILE=/root/.aws/config
export AWS_SHARED_CREDENTIALS_FILE=/root/.aws/credentials

#ejecuta el comando aws-cli sns para envio de mensajes. 
 aws sns publish --phone-number $telefono --subject "$subject" --message "$info" 2>>/tmp/out.txt 
 -----------------------------------------------------------------------------

Con 2>/tmp/out.txt coloco los mensajes de salida del SNS a un archivo donde verifico si todo esta
correcto, use las variables AWS_CONFIG_FILE, AWS_SHARED_CREDENTIALS_FILE
fueron en mi caso los que hicieron la magia , como digo no encontré otra forma en que el sistema
pudiera usar las variables que contenía las credenciales correctas.

Definimos un procedimiento java donde envío los parámetros de interés  Por pura especulación supongo que Tomcat de alguna forma ignara esta variables y que existe algún lugar donde colocarlas como en las variables que exporta como ejemplo tomcat/conf/setenv,sh , pero la la verdad ya no le gaste mas esfuerzo.
Defino la clase java para establecer el envío de mensajes.

NOTA: En algunas versiones de EC2 (particularmente en las actualizaciones de Linux2023) se presenta un error asociado con que no encuentra el modulo cryptography o algo simular.
Yo verifique que el modulo si existiera sin embargo seguía mostrando el error.
Para solucionarlo  des-instalar el aws que el sistema trae por defecto mediante:

$ sudo yum erase aws

instalar el comando pero desde phyton con pip
$ sudo pip uninstall aws
$ sudo pip uninstall awscli
e instalarlo de nuevo
$ sudo pip install awscli 
$ sudo pip install cryptography
en mi caso dejó el el comando aws en /usr/local/bin
mediante el enlace al directorio con 
$ ln /usr/local/bin/aws /usr/bin
ahora aws se ejecuto  correctamente y el script siguiente envío el mensaje.
 aws sns publish --phone-number +573185159078 --subject "ejemplo" --message "Hola mundo"
Lo despleg+o sin problema, el (+57) es el indicativo de Colombia.

SMS.java
-------------------------------------------------------------------------------------------------------------------
package dwr.zephyr ;

import java.util.* ;
import java.io.* ;
import java.text.* ;

/* Defino el comando Run permite pasar comandos a ser ejcutador por el Bash shell del sistema     operativo, podría usar el Runtime.getRunTime() directamente pero yo prefiero hacerlo así porque me permite colocar el waitFor que garantice que el comando se ejecuta antes de devolver el control a la clase que lo llama.
*/
     static public void Run(String comando )                                                                                             
                                                                                                                                              
       {                                                                                                                                    
  
        Runtime command = Runtime.getRuntime()             ;                                                             
        Process proceso = null                                                ;                                                             
        try                                                                                                                                 
            {                                                                                                                               
                proceso = command.exec(comando)                   ;                                                             
                proceso.waitFor()                                                ;                                                             
            }                                                                                                                               
        catch (Exception e)                                                                                                                 
            {                                                                                                                               
                                                                            
            }                                                                                                                               
    }                                                                                                                                                                    

public class SMS

{

static public final String SMSEnviar(String telefono,String info ,String subject ,String remitente)

{

telefono                      = telefono.trim()  ;
info                            = info.trim()         ;
String awsSNS          = new String()      ;

/* Aqui ajusto las variables a pasar al shell con guiones en lugar de blancos entre palabras
    para evitar que el shell las tome como nuevas variable.
*/

telefono = telefono.replaceAll(" ", "")   ;//Elimino los blancos de la variable telefono
subject  = subject.replaceAll(" ", "_")    ;//Cambio blancos por guiones bajo
info      = info.replaceAll(" ", "_")          ;//Cambio blancos por guiones bajo

try

{
   /* Configuro una variable de comandos parametrizada para que lo ejecute el shell */
   awsSNS = "sudo bash /usr/bin/sms.sh " + telefono + " " + subject + " " + info ;
   
    Run(awsSNS) ; 

   return "true"

}

catch (Exception e)
{
   System.out.println("Error SMS : " + e) ;
   return ("Error SMS " + e.toString()) ;

}

}

-------------------------------------------------------------------------------------------------
Así las cosas todo se reduce a conformar las variables para que comando del sistema operativo las asigne correctamente tanto de lado de tomcat como del script /usr/bin/sms.sh Colocar las credenciales entregadas por el IAM en la carpeta  (/root/.aws)
invocar el comando awsi con los argumentos correctos del SNS


}

Comentarios

Entradas populares