En muchos entornos basados en Informix 4GL, el envío de correos sigue siendo una tarea manual o poco estandarizada.
Para facilitar esta integración, he desarrollado una serie de funciones en i4GL que permiten enviar correos electrónicos de forma sencilla y reutilizable, utilizando la estructura universal de sendmail.
Aunque el ejemplo está pensado para Informix 4GL, la estructura del mensaje es perfectamente aplicable a cualquier lenguaje o sistema que invoque sendmail.
El objetivo es simplificar el proceso de envío de correos desde aplicaciones en 4GL, sin depender de librerías externas.
Las funciones permiten definir destinatarios, asunto, mensaje, adjuntos, copias (CC/BCC) y remitente, con una estructura flexible y clara.
To: usuario[arroba]dominio.com
Cc: copia[arroba]dominio.com
Bcc: oculta[arroba]dominio.com
From: no-reply[arroba]empresa.com
Subject: Prueba de envío desde 4GL
Content-Type: text/plain; charset=UTF-8
Este es el cuerpo del mensaje.
Lenguaje del código: HTTP (http)Este formato es estándar para sendmail, por lo que el mismo esquema puede utilizarse desde cualquier lenguaje (bash, Python, Java, etc.).
Solo se necesita generar el archivo temporal con esta estructura y pasarlo a sendmail mediante una llamada al sistema.
GLOBALS
DEFINE SENDMAIL_PATH CHAR(128)
DEFINE TMP_CORREO_PATH CHAR(256)
DEFINE DEFAULT_REMITENTE CHAR(128)
DEFINE DEBUG_X1SENDMAIL SMALLINT
DEFINE DEBUG_FILE_X1SENDMAIL CHAR(256)
END GLOBALS
# MAIN
# DEFINE error_correo SMALLINT
# DEFINE ficheros CHAR(10000)
# let ficheros = 'archivo_ejemplo_1.pdf,tmp/adjunto2.pdf,tmp/imagen.jpg'
# CALL x1sendmail('usuario.destino[arroba]ejemplo.com', 'Asunto de prueba desde sendmail','Cuerpo del mensaje' , 'remitente.soporte[arroba]miempresa.com', '', '', ficheros, 0) RETURNING error_correo
# DISPLAY error_correo
# IF error_correo < 1 THEN
# DISPLAY "Error al enviar el correo, error: ",error_correo
# END IF
# END MAIN
# ========================================================
# Función: x1sendmail
#
# Envía un correo electrónico con soporte para múltiples destinatarios, copia (CC), copia oculta (BCC),
# cuerpo de mensaje con saltos de línea, y adjuntos. Permite modo prueba para simular el envío.
#
# @param destinatarios Lista de destinatarios separados por coma (,). Ej: "user1[arroba]dom.com,user2[arroba]dom.com"
# @param asunto Asunto del mensaje. Máximo 100 caracteres.
# @param mensaje Puede ser una cadena de texto o la ruta de un fichero (.txt o .lis).
# Si se pasa una cadena, se admite texto plano con saltos de línea (\n), hasta 4000 caracteres.
# Si se pasa un fichero, añadirá su contenido como cuerpo del mensaje.
# @param remitente (Opcional) Dirección del remitente. Si se omite, se usará una por defecto. Máximo 300 caracteres.
# @param cc (Opcional) Lista de direcciones en copia (CC), separadas por coma.
# @param bcc (Opcional) Lista de direcciones en copia oculta (BCC), separadas por coma.
# @param rutas_txt (Opcional) Lista de rutas absolutas de los archivos adjuntos, separadas por comas.
# Cada archivo se mostrará con su nombre real (nombre del fichero) como nombre visible para el usuario.
# Ejemplo: "/ruta/f1.txt,/ruta/f2.pdf,/ruta/f3.csv"
# Máximo 4000 caracteres y hasta 60 ficheros.
# @param modo Valor 0,1,2,3
# Si es 0, envia el correo
# Si es 1, no se envía el correo
# Si es 2, no envia el correo y ACTIVA el modo DEBUG
# Si es 3, envia correo y ACTIVA el modo DEBUG
# Retorna:
# 2 ? Envío omitido por lógica (modo simulación o no autorizado)
# 1 ? Éxito
# 0 ? Error genérico
# -1 ? Faltan destinatarios
# -2 ? Falta asunto
# -3 ? Error al crear archivo de mensaje
# -4 ? Error al generar cabecera MIME
# -5 ? Error al adjuntar fichero
# -6 ? Error al ejecutar envío
# -7 ? Demasiados adjuntos (>60)
# -8 ? Tamaño de adjuntos supera el límite (25MB)
# ========================================================
FUNCTION x1sendmail(destinatarios, asunto, mensaje, remitente, cc, bcc, rutas_txt, modo)
DEFINE destinatarios CHAR(1000)
DEFINE asunto CHAR(100)
DEFINE mensaje CHAR(4000)
DEFINE remitente CHAR(300)
DEFINE cc CHAR(1000)
DEFINE bcc CHAR(1000)
DEFINE rutas_txt CHAR(4000)
DEFINE modo SMALLINT
DEFINE display_txt CHAR(10000)
DEFINE debug_txt CHAR(1000)
DEFINE enviar SMALLINT
DEFINE archivo_temp CHAR(256)
DEFINE comando CHAR(512)
DEFINE usuario CHAR(50)
DEFINE ts DATETIME YEAR TO SECOND
DEFINE temp_date CHAR(30)
DEFINE sufijo CHAR(64)
DEFINE boundary CHAR(64)
DEFINE codigo_error SMALLINT
DEFINE ruta_actual CHAR(512)
DEFINE file_bytes INTEGER
DEFINE total_bytes INTEGER
DEFINE total_mb FLOAT
DEFINE max_bytes INTEGER
DEFINE max_mb FLOAT
DEFINE txt_size
,txt_max_size CHAR(20)
DEFINE nombre_visible CHAR(100)
DEFINE tipo_mime CHAR(40)
DEFINE rutas ARRAY[60] OF CHAR(512)
DEFINE max_files SMALLINT
DEFINE i, pos, largo_txt, restante, num_rutas INTEGER
DEFINE srv_autorizados ARRAY[20] OF RECORD
srv CHAR(30)
END RECORD
DEFINE nombre_servidor CHAR(100)
DEFINE esta_autorizado SMALLINT
# Si no indica modo no hacemos nada
IF modo IS NULL THEN LET modo = 1 END IF
# Comprobar si el servidor esta autorizado para el envio de correos, si no esta en la lista se activa un modo diferente
# HOSTNAME del sistema (ej: srv-app-01, servidor-test, etc.)
LET nombre_servidor = UPSHIFT(fgl_getenv("HOSTNAME"))
LET esta_autorizado = 0
# Buscamos los servidores autorizados
LET i = 1
DECLARE srv_aut CURSOR FOR
SELECT UPPER(comentario) FROM cfg_programas WHERE codigo = "x1sendmail" AND tipo = "SR"
FOREACH srv_aut INTO srv_autorizados[i].*
LET i = i+1
-- Salimos del bucle al superar los X registros
IF i > 20 THEN EXIT FOREACH END IF
END FOREACH
FOR i = 1 TO 20
IF srv_autorizados[i].srv = nombre_servidor THEN
LET esta_autorizado = 1
EXIT FOR
END IF
END FOR
# Si el equipo no esta autorizado e indica el modo 0
IF NOT esta_autorizado AND (modo = 0 OR modo = 3) THEN LET modo = 2 END IF
# Si indica modo 1 salimos sin hacer nada
IF modo = 1 THEN RETURN 2 END IF
# En base al modo enviamos y/o mostramos
LET enviar = 0
LET DEBUG_X1SENDMAIL = 0
CASE modo
WHEN 0
-- Modo envío real
LET enviar = 1
WHEN 1
-- Modo simulación (no envía)
LET enviar = 0
WHEN 2
-- Modo debug (no envía, pero guarda trazas)
LET DEBUG_X1SENDMAIL = 1
WHEN 3
-- Envia correo y activa modo debug (para revisión)
LET enviar = 1
LET DEBUG_X1SENDMAIL = 1
END CASE
# Inicializamos variables
# LET SENDMAIL_PATH = "/usr/sbin/sendmail -t <"
LET TMP_CORREO_PATH = "/tmp/envio_correo/"
LET DEFAULT_REMITENTE = "no-reply[arroba]empresa.com"
LET SENDMAIL_PATH = "/usr/sbin/sendmail -t -f "
LET total_bytes = 0
LET max_files = 60 # Igual que el array rutas
LET max_bytes = 26214400 # 25MB
LET num_rutas = 0
# Comprobar si hay que poner correos en copia oculta
LET i = 1
DECLARE srv_bcc CURSOR FOR
SELECT comentario FROM cfg_programas WHERE codigo = 'x1sendmail' AND tipo = 'BC'
FOREACH srv_bcc INTO comando
LET i = i+1
IF LENGTH(bcc) > 0 THEN
LET bcc = bcc CLIPPED, ","
END IF
LET bcc = bcc CLIPPED, comando CLIPPED
-- Salimos del bucle al superar los X registros
IF i > 10 THEN EXIT FOREACH END IF
END FOREACH
# Si no se informa el remitente ponemos el por defecto
IF is_empty_char(remitente) THEN
LET remitente = DEFAULT_REMITENTE
END IF
# Creamos los datos 'temporales'
LET ts = CURRENT
# Usuario del sistema que ejecuta el proceso
LET usuario = fgl_getenv("USER")
LET temp_date = CURRENT YEAR TO FRACTION(4)
LET sufijo = usuario CLIPPED, "_",
temp_date[1,4], -- Año (YYYY)
temp_date[6,7], -- Mes (MM)
temp_date[9,10], -- Día (DD)
temp_date[12,13], -- Hora (HH)
temp_date[15,16], -- Min (MM)
temp_date[18,19], -- Seg (SS)
temp_date[21,24] -- Miliseg (FFFF)
# LET boundary = "====BND_" || sufijo CLIPPED || "===="
LET boundary = "====BND_Part_" ||
temp_date[1,4] || temp_date[6,7] || temp_date[9,10] || "_" ||
temp_date[12,13] || temp_date[15,16] || temp_date[18,19] || "_" ||
temp_date[21,24] || "===="
LET DEBUG_FILE_X1SENDMAIL = TMP_CORREO_PATH CLIPPED || sufijo CLIPPED || "_debug.txt"
CALL debug_sendmail("==========================================")
CALL debug_sendmail(" ENVIO DE CORREO")
CALL debug_sendmail("==========================================")
LET debug_txt = "[INFO] Servidor: ", nombre_servidor CLIPPED
CALL debug_sendmail(debug_txt)
IF esta_autorizado THEN
CALL debug_sendmail("[INFO] AUTORIZADO para el envio")
ELSE
CALL debug_sendmail("[WARNING] NO esta autorizado para el envío, solo muestra por pantalla")
END IF
CALL debug_sendmail("----------------------------------------------")
CALL debug_sendmail("[INFO] Preparación de entorno")
CALL debug_sendmail("----------------------------------------------")
# Validaciones
LET codigo_error = es_obligatorio(destinatarios, "destinatarios", -1)
IF codigo_error <> 1 THEN RETURN codigo_error END IF
LET codigo_error = es_obligatorio(asunto, "asunto", -2)
IF codigo_error <> 1 THEN RETURN codigo_error END IF
# Creamos los archivos temporales
LET archivo_temp = TMP_CORREO_PATH CLIPPED || sufijo CLIPPED || ".txt"
LET debug_txt = "[INFO] Creando la ruta temporal: ", archivo_temp CLIPPED
CALL debug_sendmail(debug_txt)
CALL debug_sendmail("")
# Separar rutas por coma
LET num_rutas = contar_ficheros(rutas_txt)
LET debug_txt = "[INFO] Ficheros detectados:",num_rutas CLIPPED
CALL debug_sendmail(debug_txt)
IF num_rutas > max_files THEN
LET debug_txt = "[ERROR] Máximo ",max_files," adjuntos permitidos. Se tienen: ", num_rutas
CALL debug_sendmail(debug_txt)
CALL limpiar_temporales(archivo_temp)
RETURN -7
END IF
# Crear el array con las rutas
FOR i = 1 TO num_rutas
LET largo_txt = LENGTH(rutas_txt)
LET pos = 0
FOR restante = 1 TO largo_txt
IF restante <= largo_txt THEN
IF rutas_txt[restante,restante] = "," THEN
LET pos = restante
EXIT FOR
END IF
END IF
END FOR
IF pos > 0 THEN
IF pos > 1 THEN
LET ruta_actual = rutas_txt[1, pos - 1] CLIPPED
ELSE
LET ruta_actual = ''
END IF
IF pos + 1 <= largo_txt THEN
LET rutas_txt = rutas_txt[pos + 1, largo_txt]
ELSE
LET rutas_txt = ''
END IF
ELSE
LET ruta_actual = rutas_txt CLIPPED
LET rutas_txt = ''
END IF
LET rutas[i] = ruta_actual
END FOR
# Crear encabezados
LET comando = "printf \"From: %s\\n\" \"" || remitente CLIPPED || "\" > " || archivo_temp
RUN comando RETURNING codigo_error
IF codigo_error <> 0 THEN
LET debug_txt = "[ERROR] Problemas al generar el fichero: ",archivo_temp
CALL debug_sendmail(debug_txt)
CALL limpiar_temporales(archivo_temp)
RETURN -4
END IF
LET comando = "printf \"To: %s\\n\" \"" || destinatarios CLIPPED || "\" >> " || archivo_temp
RUN comando
IF cc IS NOT NULL AND LENGTH(cc CLIPPED) > 0 THEN
LET comando = "printf \"Cc: %s\\n\" \"" || cc CLIPPED || "\" >> " || archivo_temp
RUN comando
END IF
IF bcc IS NOT NULL AND LENGTH(bcc CLIPPED) > 0 THEN
LET comando = "printf \"Bcc: %s\\n\" \"" || bcc CLIPPED || "\" >> " || archivo_temp
RUN comando
END IF
# LET comando = "printf \"Subject: %s\\n\" \"" || asunto CLIPPED || "\" >> " || archivo_temp
# RUN comando
# Codificar asunto
# LET comando = "printf \"Subject: =?UTF-8?B?%s?=\\n\" \"$(printf '%s' \"" || asunto CLIPPED || "\" | iconv -f ISO-8859-1 -t UTF-8 | base64 -w0)\" >> " || archivo_temp
LET comando = "printf \"Subject: =?ISO-8859-1?B?%s?=\\n\" \"$(printf '%s' \"" || asunto CLIPPED || "\" | base64 -w0)\" >> " || archivo_temp
RUN comando
# LET comando = "printf \"Date: %s\\n\" \"" || ts || "\" >> " || archivo_temp
# RFC 5322
LET comando = "LC_ALL=C date +'Date: %a, %d %b %Y %H:%M:%S %z' >> " || archivo_temp CLIPPED
RUN comando
# LET comando = "echo 'Message-ID: <" || sufijo CLIPPED || "@example.com>' >> " || archivo_temp
# Message-ID con UUID (anónimo y único)
LET comando = "echo \"Message-ID: <$(uuidgen)@example.com>\" >> " || archivo_temp CLIPPED
RUN comando
LET comando = "echo 'MIME-Version: 1.0' >> " || archivo_temp
RUN comando
LET comando = "echo 'Content-Type: multipart/mixed; boundary=\"" || boundary CLIPPED || "\"' >> " || archivo_temp
RUN comando
LET comando = "echo '' >> " || archivo_temp
RUN comando
# Crear el cuerpo
LET comando = "echo '--" || boundary CLIPPED || "' >> " || archivo_temp
RUN comando
LET comando = "echo 'Content-Type: text/plain; charset=ISO-8859-1' >> " || archivo_temp
RUN comando
LET comando = "echo 'Content-Transfer-Encoding: 8bit' >> " || archivo_temp
RUN comando
LET comando = "echo '' >> " || archivo_temp
RUN comando
# Se comprueba si el mensaje, es un texto plano o un fichero (.txt o .lis)
# Si es fichero se añade el contenido sino el texto
CALL debug_sendmail("[INFO] Se comprueba si el cuerpo del mensajes es un fichero o texto plano")
IF existe_fichero(mensaje) THEN
LET comando = "cat \"" || mensaje CLIPPED || "\" >> \"" || archivo_temp CLIPPED || "\""
ELSE
# Si el mensaje está vacío, añadir un espacio
IF is_empty_char(mensaje) THEN
LET comando = "echo ' ' >> " || archivo_temp
ELSE
LET comando = "printf \"%s\\n\" \"" || mensaje CLIPPED || "\" >> " || archivo_temp
END IF
END IF
RUN comando
CALL debug_sendmail("----------------------------------------------")
CALL debug_sendmail("[INFO] Verificación de ficheros adjuntos")
CALL debug_sendmail("----------------------------------------------")
# Adjuntos
FOR i = 1 TO num_rutas
LET ruta_actual = rutas[i]
LET nombre_visible = get_basename(ruta_actual)
IF NOT existe_fichero(ruta_actual) THEN
LET debug_txt = "[WARNING] No se adjunta (no existe): ", ruta_actual CLIPPED
CALL debug_sendmail(debug_txt)
CALL debug_sendmail(" ")
ELSE
-- Saber el tamaño del fichero
LET file_bytes = obtener_tamanio_fichero(ruta_actual)
LET total_bytes = total_bytes + file_bytes
LET txt_size = bytes_a_texto(file_bytes)
LET debug_txt = "[INFO] Adjuntando: ",nombre_visible CLIPPED, " SIZE:",txt_size CLIPPED
CALL debug_sendmail(debug_txt)
IF file_bytes > max_bytes THEN
LET debug_txt = "[WARNING] El fichero ",nombre_visible CLIPPED," supera el maximo permitido ",txt_max_size CLIPPED
CALL debug_sendmail(debug_txt)
CALL limpiar_temporales(archivo_temp)
RETURN -8
END IF
LET comando = "echo '--" || boundary CLIPPED || "' >> " || archivo_temp
RUN comando
LET tipo_mime = mime_por_extension(obtener_extension(nombre_visible))
LET comando = "printf \"Content-Type: %s; name=\\\"=?ISO-8859-1?B?%s?=\\\"\\n\" \"" || tipo_mime CLIPPED || "\" \"$(printf '%s' \"" || nombre_visible CLIPPED || "\" | base64 -w0)\" >> " || archivo_temp
# LET comando = "echo 'Content-Type: " || tipo_mime CLIPPED || "; name=\"" || nombre_visible CLIPPED || "\"' >> " || archivo_temp
RUN comando
LET comando = "echo 'Content-Transfer-Encoding: base64' >> " || archivo_temp
RUN comando
LET comando = "printf \"Content-Disposition: attachment; filename=\\\"=?ISO-8859-1?B?%s?=\\\"\\n\" \"$(printf '%s' \"" || nombre_visible CLIPPED || "\" | base64 -w0)\" >> " || archivo_temp
# LET comando = "echo 'Content-Disposition: attachment; filename=\"" || nombre_visible CLIPPED || "\"' >> " || archivo_temp
RUN comando
LET comando = "echo '' >> " || archivo_temp
RUN comando
LET comando = "base64 " || escapar_shell(ruta_actual) CLIPPED || " >> " || archivo_temp
RUN comando
CALL debug_sendmail(" ")
END IF
END FOR
IF total_bytes > max_bytes THEN
LET txt_size = bytes_a_texto(total_bytes)
LET txt_max_size = bytes_a_texto(max_bytes)
LET debug_txt = "[WARNING] El tamaño de los ficheros ",txt_size CLIPPED," supera los ",txt_max_size CLIPPED
CALL debug_sendmail(debug_txt)
CALL limpiar_temporales(archivo_temp)
RETURN -8
END IF
# Finalizar MIME
LET comando = "echo '--" || boundary CLIPPED || "--' >> " || archivo_temp
RUN comando
# Enviar correo, añadimos un timeout de X segundos para evitar bloqueos
LET comando = "timeout 60 " || SENDMAIL_PATH CLIPPED || " " || remitente CLIPPED || " < " || archivo_temp CLIPPED
IF enviar THEN
RUN comando RETURNING codigo_error
IF codigo_error <> 0 THEN
RETURN -6
END IF
END IF
CALL limpiar_temporales(archivo_temp)
CALL debug_sendmail(" ")
LET debug_txt = "[INFO] Comando de envío: ",comando CLIPPED
CALL debug_sendmail(debug_txt)
CALL debug_sendmail(" ")
CALL debug_sendmail("----------------------------------------------")
CALL debug_sendmail("[INFO] Resumen del envío simulado")
CALL debug_sendmail("----------------------------------------------")
LET debug_txt = " Remitente : ", remitente CLIPPED
CALL debug_sendmail(debug_txt)
LET debug_txt = " Destinatarios : ", destinatarios CLIPPED
CALL debug_sendmail(debug_txt)
LET debug_txt = " CC : ", cc CLIPPED
CALL debug_sendmail(debug_txt)
LET debug_txt = " BCC : ", bcc CLIPPED
CALL debug_sendmail(debug_txt)
LET debug_txt = " Asunto : ", asunto CLIPPED
CALL debug_sendmail(debug_txt)
LET debug_txt = " Mensaje : ", mensaje CLIPPED
CALL debug_sendmail(debug_txt)
CALL debug_sendmail(" ->Si es texto plano, se mostrará directamente aquí.")
CALL debug_sendmail(" ->Si es la ruta de un fichero, se mostrará la ruta y el contenido del fichero se añadirá automáticamente!!")
CALL debug_sendmail(" Adjuntos : Ver sección anterior para detalles")
CALL debug_sendmail("***Recordatorio: eliminar los ficheros temporales en /tmp/envio_correo***")
RETURN 1
END FUNCTIONEn las funciones he utilizado tanto echo como printf, según el tipo de contenido que necesitaba escribir dentro del archivo temporal del correo.
echoes más simple y rápido cuando solo se necesita imprimir texto plano, sin comillas, saltos de línea especiales o variables complejas.printf, en cambio, ofrece mayor control sobre el formato y evita errores cuando el texto incluye comillas, apóstrofes, saltos de línea (\n), o caracteres especiales que podrían romper el comandoecho.
# ========================================================
# FUNCCIONES AUXILIARES
# ========================================================
# --------------------------------------------------------
# Extrae el nombre base de un fichero desde su ruta completa.
# Ejemplo: "/tmp/archivo.txt" ? "archivo.txt"
# @param p_path Ruta completa del fichero
# @return Nombre base del fichero
# --------------------------------------------------------
FUNCTION get_basename(p_path)
DEFINE p_path CHAR(1024)
DEFINE i, pos, largo INTEGER
DEFINE ch CHAR(1)
DEFINE base CHAR(1024)
LET pos = 0
LET largo = LENGTH(p_path)
FOR i = 1 TO largo
LET ch = p_path[i]
IF ch = "/" THEN
LET pos = i
END IF
END FOR
IF pos = 0 THEN
LET base = p_path
ELSE
LET base = p_path[pos + 1, largo]
END IF
RETURN base
END FUNCTION
# --------------------------------------------------------
# Cuenta cuántos ficheros hay en una lista separada por comas.
# Ejemplo: "a.txt,b.pdf,c.csv" ? 3
# @param rutas Lista de rutas separadas por coma
# @return Número de ficheros
# --------------------------------------------------------
FUNCTION contar_ficheros(rutas)
DEFINE rutas CHAR(4001) # Tiene que tener la misma longitud que rutas_txt, de lo contrario no se sabra los ficheros exactos
DEFINE contador, i, longitud INT
DEFINE c CHAR(1)
# Si está vacío, devolver 0
IF rutas IS NULL OR LENGTH(rutas CLIPPED) = 0 THEN
RETURN 0
END IF
LET contador = 1
LET longitud = LENGTH(rutas)
FOR i = 1 TO longitud
LET c = rutas[i,i]
IF c = "," THEN
LET contador = contador + 1
END IF
END FOR
RETURN contador
END FUNCTION
# --------------------------------------------------------
# Verifica si un fichero existe en el sistema.
# @param ruta Ruta completa del fichero
# @return TRUE si existe, FALSE si no
# --------------------------------------------------------
FUNCTION existe_fichero(ruta)
DEFINE ruta CHAR(1024)
DEFINE comando CHAR(520)
DEFINE resultado SMALLINT
LET comando = "test -f ", escapar_shell(ruta) CLIPPED
RUN comando RETURNING resultado
LET ruta = "[INFO] Se comprueba si existe el fichero: test -f ",ruta CLIPPED
CALL debug_sendmail(ruta)
IF resultado = 0 THEN
RETURN TRUE
ELSE
RETURN FALSE
END IF
END FUNCTION
# --------------------------------------------------------
# Extrae la extensión de un nombre de fichero.
# Ejemplo: "archivo.pdf" ? "pdf"
# @param nombre Nombre del fichero
# @return Extensión sin punto
# --------------------------------------------------------
FUNCTION obtener_extension(nombre)
DEFINE nombre CHAR(256)
DEFINE i, pos, len INTEGER
DEFINE ext CHAR(10)
LET len = LENGTH(nombre)
LET pos = 0
FOR i = len TO 1 STEP -1
IF nombre[i,i] = "." THEN
LET pos = i
EXIT FOR
END IF
END FOR
IF pos > 0 AND pos + 1 <= len THEN
LET ext = nombre[pos + 1, len]
ELSE
LET ext = ""
END IF
LET nombre = "[INFO] Se extrae la extensión del fichero: ",nombre
CALL debug_sendmail(nombre)
RETURN ext
END FUNCTION
# --------------------------------------------------------
# Devuelve el tipo MIME correspondiente a una extensión de fichero.
# Ejemplo: "pdf" ? "application/pdf"
# @param ext Extensión del fichero
# @return Tipo MIME
# --------------------------------------------------------
FUNCTION mime_por_extension(ext)
DEFINE ext CHAR(10)
DEFINE tipo CHAR(50)
CASE ext
WHEN "pdf" LET tipo = "application/pdf"
WHEN "txt" LET tipo = "text/plain"
WHEN "csv" LET tipo = "text/csv"
WHEN "xml" LET tipo = "application/xml"
WHEN "html" LET tipo = "text/html"
WHEN "doc" LET tipo = "application/msword"
WHEN "docx" LET tipo = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
WHEN "xls" LET tipo = "application/vnd.ms-excel"
WHEN "xlsx" LET tipo = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
WHEN "jpg" LET tipo = "image/jpeg"
WHEN "jpeg" LET tipo = "image/jpeg"
WHEN "png" LET tipo = "image/png"
WHEN "gif" LET tipo = "image/gif"
WHEN "zip" LET tipo = "application/zip"
WHEN "json" LET tipo = "application/json"
OTHERWISE LET tipo = "application/octet-stream"
END CASE
CALL debug_sendmail("[INFO] Se mira el MIME del fichero, en base a la extensión")
RETURN tipo CLIPPED
END FUNCTION
# --------------------------------------------------------
# Verifica si un campo CHAR está vacío o nulo.
# @param valor Valor a verificar
# @return 1 si está vacío, 0 si tiene contenido
# --------------------------------------------------------
FUNCTION is_empty_char(valor)
DEFINE valor CHAR
IF valor IS NULL OR valor = '' THEN
RETURN 1
ELSE
RETURN 0
END IF
END FUNCTION
# --------------------------------------------------------
# Valida si un campo obligatorio está vacío. Muestra error si modo = 1.
# @param valor Valor a validar
# @param nombre Nombre del campo (para mostrar en error)
# @param codigo_error Código de error a devolver si falla
# @return 1 si válido, código_error si falla
# --------------------------------------------------------
FUNCTION es_obligatorio(valor, nombre, codigo_error)
DEFINE valor CHAR(1000)
DEFINE nombre CHAR(50)
DEFINE codigo_error SMALLINT
DEFINE modo SMALLINT
DEFINE len INTEGER
LET len = LENGTH(valor CLIPPED)
IF valor IS NULL OR len = 0 THEN
LET nombre = "[INFO] Campo ",nombre, "es obligatorio, valor de la variable: ",valor
CALL debug_sendmail(nombre)
RETURN codigo_error
END IF
RETURN 1
END FUNCTION
# --------------------------------------------------------
# Saber el tamaño del fichero
# @param ruta La ruta del fichero
# --------------------------------------------------------
FUNCTION obtener_tamanio_fichero(ruta)
DEFINE ruta CHAR(512)
DEFINE comando CHAR(600)
DEFINE archivo_temp CHAR(256)
DEFINE tam_fichero INTEGER
DEFINE num_rows INTEGER
DEFINE usuario CHAR(50)
DEFINE temp_date CHAR(30)
DEFINE sufijo CHAR(64)
DEFINE debug_txt CHAR(2000)
LET usuario = fgl_getenv("USER")
LET temp_date = CURRENT YEAR TO FRACTION(4)
LET sufijo = usuario CLIPPED, "_",
temp_date[1,4], -- Año (YYYY)
temp_date[6,7], -- Mes (MM)
temp_date[9,10], -- Día (DD)
temp_date[12,13], -- Hora (HH)
temp_date[15,16], -- Min (MM)
temp_date[18,19], -- Seg (SS)
temp_date[21,24] -- Miliseg (FFFF)
LET archivo_temp = TMP_CORREO_PATH CLIPPED || sufijo CLIPPED || "_size_file.txt"
LET debug_txt = "[INFO] Obteniendo el tamaño del fichero ",ruta CLIPPED,", creando fichero temporal: ",archivo_temp CLIPPED, ", para pasarlo a la base de datos"
CALL debug_sendmail(debug_txt)
# Borramos la tabla temporal
# WHENEVER ERROR CONTINUE
# DROP TABLE tmp_size_file_sendmail
# IF SQLCA.SQLCODE < 0 THEN
# LET debug_txt = "Error al eliminar 1 tmp_size_file_sendmail: SQLCODE = ", SQLCA.SQLCODE
# CALL debug_sendmail(debug_txt)
# END IF
# WHENEVER ERROR STOP
CALL debug_sendmail("[INFO] Se crea la tabla temporal para saber el tamaño del fichero")
# Creamos una tabla temporal para poder guardar el tamaño, ya que desde el fichero no podemos recuperarlo
CREATE TEMP TABLE tmp_size_file_sendmail(size_file INTEGER)
IF SQLCA.SQLCODE < 0 THEN
LET debug_txt = "[ERROR] Error al crear la tabla tmp_size_file_sendmail: SQLCODE = ", SQLCA.SQLCODE
CALL debug_sendmail(debug_txt)
END IF
# LET comando = "wc -c < \"" || ruta CLIPPED || "\" > " || archivo_temp CLIPPED || " 2>/dev/null"
LET comando = "stat -c %s " || escapar_shell(ruta) CLIPPED || " > " || archivo_temp CLIPPED || " 2>/dev/null"
LET debug_txt = "[INFO] ",comando
CALL debug_sendmail(debug_txt)
RUN comando
LOAD FROM archivo_temp INSERT INTO tmp_size_file_sendmail
# Comprobamos si hay datos, sino devolvemos 0
SELECT COUNT(*) INTO num_rows FROM tmp_size_file_sendmail
IF num_rows > 0 THEN
SELECT size_file INTO tam_fichero FROM tmp_size_file_sendmail
ELSE
LET tam_fichero = 0 # No se pudo obtener el tamaño
END IF
# Limpiar archivo temporal
CALL limpiar_temporales(archivo_temp)
# Borramos la tabla temporal
WHENEVER ERROR CONTINUE
DROP TABLE tmp_size_file_sendmail
IF SQLCA.SQLCODE < 0 THEN
LET debug_txt = "[ERROR] Error al eliminar 2 tmp_size_file_sendmail: SQLCODE = ", SQLCA.SQLCODE
CALL debug_sendmail(debug_txt)
END IF
WHENEVER ERROR STOP
RETURN tam_fichero
END FUNCTION
# --------------------------------------------------------
# Convierte bytes a formato legible (KB o MB)
# @param bytes Tamaño en bytes
# @return String formateado (ej: "1.5 MB" o "500 KB")
# --------------------------------------------------------
FUNCTION bytes_a_texto(bytes)
DEFINE bytes INTEGER
DEFINE resultado CHAR(20)
DEFINE kb, mb DECIMAL(10,2)
IF bytes < 1024 THEN
LET resultado = bytes USING "######", " bytes"
ELSE
IF bytes < 1048576 THEN # Menos de 1 MB
LET kb = bytes / 1024.0
LET resultado = kb USING "###.##", " KB"
ELSE
LET mb = bytes / 1048576.0
LET resultado = mb USING "###.##", " MB"
END IF
END IF
RETURN resultado CLIPPED
END FUNCTION
# --------------------------------------------------------
# Limpia archivos temporales
# @param archivo_temp Ruta del archivo temporal a eliminar
# --------------------------------------------------------
FUNCTION limpiar_temporales(archivo_temp)
DEFINE archivo_temp CHAR(256)
DEFINE comando CHAR(512)
IF NOT DEBUG_X1SENDMAIL THEN
LET comando = "rm -f " || escapar_shell(archivo_temp) CLIPPED
RUN comando
END IF
END FUNCTION
# --------------------------------------------------------
# Escapa caracteres para el shell
# @param nombre Se escapan los caracteres 'raros' para que funcione en shell
# --------------------------------------------------------
FUNCTION escapar_shell(texto)
DEFINE texto CHAR(1024)
DEFINE texto_seguro CHAR(1512)
DEFINE i, largo INTEGER
DEFINE c CHAR(1)
LET texto_seguro = texto
# LET texto_seguro = "";
# FOR i = 1 TO LENGTH(texto)
# LET c = texto[i]
# CASE c
# WHEN "\\" LET texto_seguro = texto_seguro CLIPPED || "\\\\"
# WHEN "\"" LET texto_seguro = texto_seguro CLIPPED || "\\\""
# WHEN "$" LET texto_seguro = texto_seguro CLIPPED || "\\$"
# WHEN "`" LET texto_seguro = texto_seguro CLIPPED || "\\`"
# WHEN " " LET texto_seguro = texto_seguro CLIPPED || "\\ "
# WHEN "'" LET texto_seguro = texto_seguro CLIPPED || "'\\''"
# OTHERWISE LET texto_seguro = texto_seguro , c
# END CASE
# END FOR
LET texto_seguro = "\"" , texto_seguro CLIPPED , "\""
RETURN texto_seguro CLIPPED
END FUNCTION
# --------------------------------------------------------
# Detecta si en el cuerpo del mensaje es un texto o un listado
# @param nombre Text o fichero.txt/lis
# --------------------------------------------------------
FUNCTION message_body(nombre)
DEFINE nombre CHAR(30000)
DEFINE txt CHAR(30000)
LET txt = nombre
IF existe_fichero(txt) THEN
# Borramos la tabla temporal
WHENEVER ERROR CONTINUE
DROP TABLE tmp_body_message
WHENEVER ERROR STOP
# Creamos una tabla temporal
CREATE TEMP TABLE tmp_body_message(body CHAR(30000))
# Cargamos el fichero en la DB
LOAD FROM nombre INSERT INTO tmp_body_message
# Seleccionar el texto
SELECT body INTO txt FROM tmp_body_message
# Borramos la tabla temporal
WHENEVER ERROR CONTINUE
DROP TABLE tmp_body_message
WHENEVER ERROR STOP
END IF
RETURN txt
END FUNCTION
# --------------------------------------------------------
# DEBUG
# @param text El texto que queremos añadir al fichero de debug
# --------------------------------------------------------
FUNCTION debug_sendmail(texto)
DEFINE texto CHAR(2000)
DEFINE comando CHAR(1000)
IF DEBUG_X1SENDMAIL THEN
LET comando = "printf \"%s\\n\" \"" || texto CLIPPED || "\" >> " || DEBUG_FILE_X1SENDMAIL
RUN comando
END IF
END FUNCTION
########################################################################
# Función para generar el campo Date según RFC 5322 usando SQL
# Formato: Date: Day, DD Mon YYYY HH:MM:SS +ZZZZ
# Ejemplo: Date: Sat, 01 Jan 2000 12:00:59 +0200
# Con detección EXACTA de horario verano/invierno
########################################################################
FUNCTION rfc5322_date()
DEFINE v_rfc_date CHAR(50)
DEFINE v_timezone CHAR(6)
DEFINE v_hoy DATE
LET v_hoy = TODAY
# 1. Determinamos el Timezone
IF is_dst_spain(v_hoy) THEN
LET v_timezone = "+0200"
ELSE
LET v_timezone = "+0100"
END IF
# 2. Generamos la fecha
# IMPORTANTE: La DB tiene que devolver los meses en INGLÉS (Jan, Feb...)
SELECT TO_CHAR(CURRENT, '%a, %d %b %Y %H:%M:%S')
INTO v_rfc_date
FROM informix.systables
WHERE tabid = 1
LET v_rfc_date = v_rfc_date CLIPPED, " ", v_timezone CLIPPED
RETURN v_rfc_date
END FUNCTION
########################################################################
# Función para determinar si una fecha está en horario de verano (DST)
# para España/Europa
# Retorna: 1 = Verano (CEST), 0 = Invierno (CET)
########################################################################
FUNCTION is_dst_spain(p_date)
DEFINE p_date DATE
DEFINE y SMALLINT
DEFINE mar31 DATE
DEFINE oct31 DATE
DEFINE start_dst DATE
DEFINE end_dst DATE
LET y = YEAR(p_date)
# Cálculo del último domingo de marzo
LET mar31 = MDY(3, 31, y)
LET start_dst = mar31 - WEEKDAY(mar31)
# Cálculo del último domingo de octubre
LET oct31 = MDY(10, 31, y)
LET end_dst = oct31 - WEEKDAY(oct31)
IF p_date >= start_dst AND p_date < end_dst THEN
RETURN 1 # Verano (CEST +0200)
ELSE
RETURN 0 # Invierno (CET +0100)
END IF
END FUNCTION
⚠️Las funciones están operativas, pero es posible que todavía quede algún comportamiento por afinar o algún caso no contemplado.
⚠️Si detectáis cualquier bug o mejora posible, comentádmelo y lo revisamos.
Al integrar estas funciones en un entorno productivo, conviene ajustar algunos detalles:
- Ruta de archivos temporales.
- Tamaño máximo de adjuntos.
- Política de borrado de ficheros tras el envío.
- Logs o trazabilidad del proceso.
Ejemplo:
CALL x1sendmail(
'usuario[arroba]ejemplo.com',
'Asunto de prueba',
'Cuerpo del mensaje',
'no-reply[arroba]empresa.com',
'', '', ficheros, 0
)Lenguaje del código: JavaScript (javascript)Gracias a este conjunto de funciones, el envío de correos desde Informix 4GL se simplifica notablemente y adopta un formato universal compatible con cualquier entorno basado en sendmail.
Esto permite integrar notificaciones automáticas, alertas o reportes sin depender de soluciones externas.