Envío de correos desde Informix 4GL con sendmail

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 FUNCTION

En las funciones he utilizado tanto echo como printf, según el tipo de contenido que necesitaba escribir dentro del archivo temporal del correo.

  • echo es 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 comando echo.

# ========================================================
# 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.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Captcha cargando...

Scroll al inicio