miércoles, 8 de octubre de 2014

Envío de Correos utilizando UTL_SMTP 1.2

Después de mucho tiempo de no publicar nada, el día de hoy publico la nueva versión del procedimiento de envío de correos con adjuntos utilizando UTL_SMTP.

He de agradecer a todos los que han realizado comentarios sobre dudas, errores y ajustes que han sido necesarios realizar al programa.

Los cambios son menores (de hecho van comentados en un pequeño encabezado) y con se han realizado correcciones a algunos errores detectados.

Espero a partir de hoy, poder realizar publicaciones con mayor regularidad para seguir compartiendo y aprendiendo de mis lectores.

Les dejo el código, para ejecutarlo se utiliza la misma sentencia que está en el post Envío de Correos utilizando UTL_SMTP

-- Envio de Correos a multiples destinatarios con multiples adjuntos utilizando UTL_SMTP                                  
PROCEDURE P_SendMailAttach(p_DestinatariosPara     IN VARCHAR2 CHARACTER SET ANY_CS, 
                           p_DestinatariosCC       IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT NULL,
                           p_DestinatariosBcc      IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT NULL,
                           p_Mensaje               IN LONG DEFAULT 'Haga caso omiso a este mensaje',
                           p_Remitente             IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT 'correo@dominio.com',
                           p_Asunto                IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT 'Este correo no tiene asunto',
                           p_TipoMensaje           IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT 'TEXT',
                           p_Directorio            IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT 'ATTACHMENTS',
                           p_ArchivosAdjuntos      IN VARCHAR2 CHARACTER SET ANY_CS)
AS
-----------------------------------------------------------------------------------------------
-- Programa     : P_SendMailAttach
-- Descripción  : Procedimiento  para  el  envío   de  correos  planos  y  HTML  con  adjuntos
--                utilizando el package UTL_SMTP.
-- Notas        : Los listados de remitentes deberán contener las direcciones separadas por un
--                colon (,)
--                El mensaje por default esta definido como texto plano
--                El Cuerpo del Mensaje no debe superar los 32K (Resolved)

--   When         Who                         What

--   ===========  ======================      =================================================
--   21-MAR-2012  Rogelio Rodríguez           Initial Creation
--
--   16-APR-2012  Rogelio Rodríguez           Se agrega la clausula CHARACTER SET ANY_CS   para
--                                            permitir el uso de caracteres  especiales  en los
--                                            valores de tipo VARCHAR2 de manera automática
--   16-APR-2012  Rogelio Rodríguez           Se modifican  las  rutinas  para la separación de
--                                            Items, cambiando  el  reemplaxo  de  espacios  en
--                                            blanco por un TRIM y un  replace '; ' que  es  lo
--                                            más  común
--
--   20-JUN-2012  Rogelio Rodriguez           Se modifica el envío del mensaje y asunto, utili-
--                                            zando  la  conversión  a RAW  para que respete el
--                                            juego  de  caracteres  iso-8859-1 en los procedi-
--                                            mientos SendMail y SendMailAttach.
--
--   15-OCT-2012  Rogelio Rodríguez           Se modifica la variable p_Mensaje de VARCHAR2

--                                            a LONG para que no tenga el límite de los 32K.

--
--   08-OCT-2014  Rogelio Rodríguez           Se modifica el orden de inserción de  encabezados
--                                            y límites para la separación de  adjuntos  ya que
--                                            bajo  el  servidor  SMTP  Exchange  2010  no   se
--                                            realizaba  de  manera  correcta  la  adición   de
--                                            Adjuntos en el procedimiento SendMailAttach.
----------------------------------------------------------------------------------------------- 

c utl_smtp.connection;

l_ServidorSMTP VARCHAR2(30);
l_PuertoSMTP   VARCHAR2(10);
l_MensajeRaw   LONG RAW;

--Procedimiento para escritura de Headers

-----------------------------------------------------------------
PROCEDURE send_header(name IN VARCHAR2, header IN VARCHAR2) AS 
BEGIN 
utl_smtp.write_data(c, name || ': ' || header || utl_tcp.CRLF); 
END;
-----------------------------------------------------------------



--Procedimiento para Adición de Destinatarios

-----------------------------------------------------------------------------------------------------------------------------------
PROCEDURE add_rcpt(p_Destinatarios in VARCHAR2) AS
l_Cadena varchar2(500) := p_Destinatarios;
l_LargoCadena number;
l_Comas number;
l_PosicionComa number := 0;
l_Destinatario varchar2(100);
BEGIN

l_Cadena := replace(l_Cadena,', ',',');

l_Cadena := trim(l_Cadena);
l_LargoCadena := length(l_Cadena);
l_Comas := l_LargoCadena-length(replace(l_Cadena,','));

--Bloque 1 Asginación del RCPT

    IF l_Comas > 0 THEN
          FOR l_segmento IN 1 .. l_Comas LOOP
            
              l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, instr(l_Cadena,',',1,l_segmento) - (l_PosicionComa + 1));
            
              l_PosicionComa := instr(l_Cadena,',',1,l_Segmento);
              utl_smtp.rcpt(c, l_Destinatario);
              
          END LOOP;
    END IF;
-- Fin de Bloque 1

-- Bloque 2: Para inserción del ultimo recipient solicitado (o el primero, si es unico)

    l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, l_LargoCadena);
    utl_smtp.rcpt(c, l_Destinatario);
-- Fin Bloque 2  

END;

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


-- Procedimiento de adición de cabeceras para destinatarios

---------------------------------------------------------------------------------------------------------------------------------
PROCEDURE add_headers_rcpt(p_Destinatarios IN VARCHAR2, p_Type IN VARCHAR2) AS
l_Cadena varchar2(500) := p_Destinatarios;
l_LargoCadena number;
l_Comas number;
l_PosicionComa number := 0;
l_Destinatario varchar2(100);
BEGIN

l_Cadena := replace(l_Cadena,', ',',');

l_Cadena := trim(l_Cadena);
l_LargoCadena := length(l_Cadena);
l_Comas := l_LargoCadena-length(replace(l_Cadena,','));

--Bloque 1 Asginación del Destinatario al header

    IF l_Comas > 0 THEN
          FOR l_segmento IN 1 .. l_Comas LOOP
            
              l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, instr(l_Cadena,',',1,l_segmento) - (l_PosicionComa + 1));
            
              l_PosicionComa := instr(l_Cadena,',',1,l_Segmento);
              
              -- Se generan los encabezados para envio, en caso de Bcc no se agrega el Header
              IF p_Type = 'TO' THEN
                send_header('To', l_Destinatario);
              ELSE
                send_header('Cc', l_Destinatario);
              END IF;
              
          END LOOP;
    END IF;
-- Fin de Bloque 1

-- Bloque 2: Para inserción del ultimo recipient solicitado (o el primero, si es unico)

    l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, l_LargoCadena);
    IF p_Type = 'TO' THEN
       send_header('To', l_Destinatario);
    ELSE
       send_header('Cc', l_Destinatario);
    END IF;
-- Fin Bloque 2  

END;

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


-- Procedimiento para adjuntar los archivos al stream del correo

---------------------------------------------------------------------------------------------------------------------
PROCEDURE file_attach(p_Archivo varchar2) AS
-- Variables para el procesamiento de Archivos
 rfile     RAW(57);
 flen      NUMBER;
 bsize     NUMBER;
 src_file  bfile;
 buffer_   integer := 57;
 i         integer := 1;

BEGIN


    -- Escribir cabecera MIME

    utl_smtp.write_data(c,'--MIME.Bound'||utl_tcp.CRLF);
    send_header('Content-Type','application/octet-stream; name="' || p_Archivo || '"');
    send_header('Content-Transfer-Encoding', 'base64' );
    send_header('Content-Disposition', 'attachment; filename="' || p_Archivo || '"');
    utl_smtp.write_data(c, utl_tcp.CRLF);
    
       
    -- Adición del Archivo 
    src_file := bfilename(p_Directorio, p_Archivo); 
              flen := dbms_lob.getlength(src_file);
              
              dbms_lob.fileopen(src_file, dbms_lob.file_readonly);
              
              while i < flen loop
                    dbms_lob.read( src_file, buffer_, i, rfile );
                    utl_smtp.write_raw_data(c, utl_encode.base64_encode(rfile));
                    utl_smtp.write_data(c, utl_tcp.CRLF);
                    i := i + buffer_;
              end loop while_loop;
              
              dbms_lob.fileclose(src_file);
              utl_smtp.write_data(c, utl_tcp.CRLF||utl_tcp.CRLF);

END;

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


--Procedimiento para separación y Adición de Archivos Adjuntos

------------------------------------------------------------------------------------------------------------------------------------
PROCEDURE add_attachments(p_Adjuntos in VARCHAR2) AS
l_Cadena varchar2(500) := p_Adjuntos;
l_LargoCadena number;
l_Comas number;
l_PosicionComa number := 0;
l_Archivo varchar2(100);

-- Variables para el procesamiento de Archivos

 rfile     RAW(57);
 flen      NUMBER;
 bsize     NUMBER;
 src_file  bfile;
 buffer_   integer := 57;
 i         integer := 1;

BEGIN


l_Cadena := replace(l_Cadena,', ',',');

l_Cadena := trim(l_Cadena);
l_LargoCadena := length(l_Cadena);
l_Comas := l_LargoCadena-length(replace(l_Cadena,','));

--Bloque 1 Adición del Archivo

    IF l_Comas > 0 THEN
          FOR l_segmento IN 1 .. l_Comas LOOP
          
              l_Archivo := substr(l_Cadena, l_PosicionComa + 1, instr(l_Cadena,',',1,l_segmento) - (l_PosicionComa + 1));
              dbms_output.put_line(l_Archivo);
              file_attach(l_Archivo);
            
              l_PosicionComa := instr(l_Cadena,',',1,l_Segmento);
              
          END LOOP;
    END IF;
-- Fin de Bloque 1

-- Bloque 2: Para inserción del ultimo archivo (o el primero, si es unico)

    l_Archivo := substr(l_Cadena, l_PosicionComa + 1, l_LargoCadena);
    file_attach(l_Archivo);
              
-- Fin Bloque 2  

END;

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


-- Inicio del Programa Core para el correo

BEGIN 

    -- Bloque de Apertura de Conexión

    l_ServidorSMTP := F_RetornaValorParametro('LOCAL_SERVER_MAIL');
    l_PuertoSMTP   := F_RetornaValorParametro('LOCAL_SERVER_MAIL_PORT');
    
    c := utl_smtp.open_connection(l_ServidorSMTP,l_PuertoSMTP); 
    utl_smtp.helo(c, l_ServidorSMTP); 
    utl_smtp.mail(c, p_Remitente);-- Remitente 

    -- Bloque de Adición de Destinatarios

    IF p_DestinatariosPara IS NOT NULL THEN
        add_rcpt(p_DestinatariosPara);
    END IF;

    IF p_DestinatariosCC IS NOT NULL THEN

        add_rcpt(p_DestinatariosCC);
    END IF;

    IF p_DestinatariosBcc IS NOT NULL THEN

        add_rcpt(p_DestinatariosBcc);
    END IF;

    --Bloque de Apertura de Datos

    utl_smtp.open_data(c); 

    -- Bloque de Adición de Cabeceras de Mail

    IF p_DestinatariosPara IS NOT NULL THEN
        add_headers_rcpt(p_DestinatariosPara, 'TO');
    END IF;

    IF p_DestinatariosCC IS NOT NULL THEN

        add_headers_rcpt(p_DestinatariosCC, 'CC');
    END IF;
    
    -- Demás Cabeceras
    send_header('From', p_Remitente); -- De
    send_header('MIME-Version','1.0');
    send_header('Content-Type','multipart/mixed; boundary="MIME.Bound"');
    
    -- Escritura del Asunto
    l_MensajeRaw := utl_raw.cast_to_raw('Subject:'||p_Asunto);
    utl_smtp.write_raw_data(c, l_MensajeRaw);
    utl_smtp.write_data(c, utl_tcp.CRLF);
    
    -- Inicia bloques de escritura de Mensaje y Archivos con sus respectivos Boundaries
    utl_smtp.write_data(c,'--MIME.Bound'||utl_tcp.CRLF);
    
    IF p_Mensaje IS NOT NULL THEN
    
        IF p_TipoMensaje = 'TEXT' THEN
            send_header('MIME-Version','1.0');
            send_header('Content-Type','text/plain; charset=iso-8859-1');
            send_header('Content-Disposition', 'inline');
        ELSE
            send_header('MIME-Version','1.0');
            send_header('Content-Type', 'text/html;charset=iso-8859-1');
            send_header('Content-Disposition', 'inline');
            send_header('Content-Transfer-Encoding', '8bit');
        END IF;
    
    END IF;

    -- Bloque de Escritura del Mensaje 

    l_MensajeRaw := utl_raw.cast_to_raw(p_Mensaje); 
    utl_smtp.write_data(c, utl_tcp.CRLF);
    utl_smtp.write_raw_data(c, l_MensajeRaw);
    utl_smtp.write_data(c, utl_tcp.CRLF);
    utl_smtp.write_data(c, utl_tcp.CRLF);
    
    -- Bloque de Envio de Adjuntos
    add_attachments(p_ArchivosAdjuntos);
    utl_smtp.write_data(c, utl_tcp.CRLF||'--MIME.Bound'||utl_tcp.CRLF);

    -- Bloque de Cierre de Datos y Envio del Mail

    utl_smtp.close_data(c); 

    -- Bloque de Cierre de Conexión

    utl_smtp.quit(c); 

    EXCEPTION

     WHEN utl_smtp.Transient_Error OR utl_smtp.Permanent_Error then
        -- Bloque de Cierre de Conexión
        utl_smtp.quit(c); 
       raise_application_error(-20100, 'Unable to send mail: '||sqlerrm);
     WHEN OTHERS THEN
        -- Bloque de Cierre de Conexión
        utl_smtp.quit(c); 
        raise_application_error(-20100, 'Unable to send mail: '||sqlerrm);
    


END P_SendMailAttach;


Espero nuevamente les sea de utilidad el código, me llevo de tarea una mejor implementación del mismo de forma tal que las funciones y procedimientos internos puedan ser reutilizables.

Así mismo espero sus comentarios sobre funcionamiento u otras mejoras a agregar.

jueves, 16 de agosto de 2012

Implementación de una Réplica Standalone de Base de Datos Oracle 11g

Hace mucho que no escribo en este espacio, pero entre el trabajo y las actividades en el hogar, no queda mucho tiempo para escribir.

En esta entrega mostraré los pasos para implementar una réplica standby de Base de Datos sobre Windows. He de mencionar que esto lo hice en base a la documentación de Oracle para implementar replicas sin dataguard para la versión 9i, que estaba guardado en mi empresa.

Este tipo de réplica aún es utilizado en muchos lugares, ya que la implementación no es muy compleja y en cuestiones de licenciamiento, no tiene costo mas que el de la BD que se está creando.

Actualmente lo tengo aplicado con la versión 11g, y una de las ventajas es que se puede utilizar la modalidad snapshot database, útil para pruebas de tipo UAT (User Aceptance Test) sin necesidad de que entren a los entornos productivos o hacer carga de información a un entorno de desarrollo.

Las condiciones para implementar la réplica son las siguientes:

  • Las version de la BD primaria y la réplica deben ser la misma
  • La versión sistema operativo donde se encuentra la BD primaria debe ser del mismo tipo que la réplica (no se pueden mezclar linux en la primaria con un windows en la réplica, por ejemplo)


Ahora si, vamos a colocar los pasos (recuerden que la implementación que describo es para un ambiente con SO Windows Server 2003, en caso de utilizar otro hay que revisar en la documentación de oracle particularidades para su SO.

En el servidor de Producción

1.- Realizar un respaldo en Frío de la Base de Datos (Datafiles, Controlfiles, Redologs, PWDFile y SPFILE)

2.- Crear un PFILE a partir del SPFILE y colocarlo en la carpeta del respaldo en Frío
      SQL> create pfile = 'ruta\SIDPfile.ora' from spfile;

3.- Crear un STANDBY CONTROLFILE y colocarlo en la carpeta del respaldo en Frío
      SQL>alter database create standby controlfile as 'ruta\standby.ctl';

4.- Copiar la carpeta del respaldo en Frío en el servidor para la réplica.


En el servidor de Réplica

5.- Instalar software de Base de Datos Oracle con la misma versión que en el de Producción

6.-Crear una instancia con DBCA la cual deberá tener el mismo SID que la de Producción

7.- Modificar el PFILE de la carpeta del respaldo en frío

  • Modificar rutas y nombres de los controlfiles por los de standby control files en el parametro *.control_files
  • Modificar ruta de destino de archives (si aplica)
  • Si los datafiles no van a quedar en la misma ruta que en el servidor de producción, utilizar el parametro *.db_file_name_convert para hacer las "traducciones de rutas"
  • Si los redolog no estarán en la misma ruta que en el servidor de producción utilizar el parametro  *.log_file_name_convert (Se pueden utilizar "patrones" de rutas, por ejemplo si yo seteo en la ruta "C:\DIR\FILES\" se traduciran todos las carpetas que estén en la raíz de esa ruta [FILES] y lo que esté dentro de ella)
8.- En una consola de SQLPlus crear un SPFILE a partir del PFILE que se modificó, con ello se creará un nuevo SPFILE en la ruta del original


      SQL>create spfile from pfile='ruta\SIDpfile.ora';

      NOTA: En este paso asegurese que sus variables de entorno están configuradas para el SID de la base de datos que creó con DBCA

9.- Detener la instancia e iniciarla en modo mount


      SQL>shutdown immediate;
      SQL>startup mount;

       NOTA: Si al momento de montar la base no reporta ningún datafile faltante, quiere decir que vamos bien... pero pase lo que pase NO ABRIR LA BASE DE DATOS!

10.- Pasar la Base de Datos a modo Physical Standby, con ello al abrir la BD automáticamente se abre en modo SOLO LECTURA. Esta operación desmonta la base de datos.

       SQL>alter database convert to physical standby

Para revisar si la configuración aplicó realice la siguiente consulta:


select database_role from v$database;

11.- Reiniciar la Base de Datos en modo MOUNT


SQL>shutdown immediate;
      SQL>startup mount;


12.- Iniciar el proceso de recuperación

SQL>recover stanby database;

Con ello dará inicio el proceso de recuperación, el cual solicitará los archives que sean necesarios para completar los datafiles con las operaciones realizadas en la BD de Producción.

Este proceso se puede realizar por medio de archivos de procesamiento por lotes.

Activación de la Base de Datos Standby 

Para activar la Base de Datos en modo Lectura/Escritura en caso de perdida del servidor productivo, se realiza lo siguiente:

1.- Detener el proceso de sincronización
2.- Detener instancia de Base de Datos
3.- Iniciar la instancia en modo MOUNT
4.- Pasar la Base de Datos de Physical Standby a Primary

      SQL>alter database activate physical standby database;

5.- Verificar el rol de la base de datos ejecutando la consulta, de la cual es resultado debe ser PRIMARY

SQL>select database_role from v$database;


6.- Abrir la instancia de Base de Datos y verificar que este abierta para operaciones de lectura y escritura. El resultado de la siguiente consulta debe ser, para OPEN_MODE = READ WRITE y para DATABASE_ROLE=PRIMARY


SQL>select open_mode, database_role from v$database;


Y con ello concluimos la implementación de la réplica de Base de Datos.

Mi proceso de sincronización lo describo en la siguiente imagen




También les dejo los scripts que utilizo en .bat para transeferir los archives y hacer la recuperación en la BD.

Script de Transferencia de Producción a Réplica por FTP

TITLE Envio Replica CTS
:INICIO
cls
@ECHO OFF
cd G:\DatabaseProd\Arcs
if exist *.txt del *.txt
if not exist *.arc goto :FINAL
ren *.arc *.bck
dir /B *.bck >>ListaArchives.txt
echo open ftp-replica >>ftpenvio.txt
echo userftp>>ftpenvio.txt
echo pwd_userftp>>ftpenvio.txt
for /F "eol=t tokens=1" %%i in (ListaArchives.txt) do echo put %%i>>ftpenvio.txt
echo bye>>ftpenvio.txt
ftp -s:ftpenvio.txt
move *.bck G:\DatabaseProd\ArcsProcesados
ren  G:\DatabaseProd\ArcsProcesados\*.bck *.ARC
:FINAL
timeout 120
GOTO :INICIO


Scripts de Recuperación en Réplica


@ECHO OFF
TITLE Recover Replica CTS
:RECOVER
cd C:\Archives
if not exist *.bck goto :FINAL
ren *.bck *.ARC
sqlplus / as sysdba @SQLRecover.sql
:FINAL
timeout 120
if exist *.ARC move *.ARC O:\ArchivesCargados
goto :RECOVER


El script SQLRecover.sql tiene el siguiente contenido

recover standby database;
auto
quit

Esta ocasión si ha sido un post extenso, pero que espero sea de utilidad.

Posteriormente estaré probando Oracle Dataguard, ya que lo implemente registraré mi experiencia aqui.



miércoles, 18 de abril de 2012

Envío de Correos utilizando UTL_SMTP


Nueva Versión!
Se ha publicado una actualización al procedimiento en el siguiente enlace

Puedes seguir leyendo este post como referencia.

Desde que inicié en el mundo Oracle, me he encontrado con muchas implementaciones que te permiten hacer ciertos procesos desde la BD sin necesidad de hacerlos desde un programa externo. En esta ocasión veremos el envío de correos.

Tanto en la documentación de Oracle como en la web encontrarás muchos ejemplos y documentación, sin embargo ninguno (creo yo aplicado a un entorno real) y sin "partes ocultas".

A continuación mostraré un procedimiento que utiliza el package UTL_SMTP con el cual se pueden enviar correos a múltiples destinatarios en el Para, CC y CCO, así como múltiples adjuntos.

He de mencionar que tiene sus requerimientos.

  1. La lista de destinatarios y de archivos debe estar separadas por punto y coma (;) y sin espacios entre ellos (esta condición la pueden cambiar modificando el fuente), Ejemplo: 'usuario.uno@dominio.com;usuario.dos@dominio.dos.mx;usuario.tres@otrodominio.com'
  2. El procedimiento hace uso de un directorio lógico en donde se alojan los archivos a adjuntar, por lo que es necesario creen el directorio con el nombre que requieran en su BD y ahi coloquen los archivos.
  3. Se debe especificar el tipo de correo, ya sea HTML (en el parametro se coloca 'HTML') o Texto, que es el valor por default.
Ya con esto aclarado y recordandoles que hay comentarios dentro del código, puedo decirles que con este ejemplo pueden basarse para hacer uno sin envío de adjuntos (innecesario para mi gusto, pues con el solo hecho de mandar la cadena vacía, no se manda nada).


create or replace procedure P_SendMailAttach

(

p_DestinatariosPara     VARCHAR2, 

p_DestinatariosCC       VARCHAR2 DEFAULT NULL,

p_DestinatariosBcc      VARCHAR2 DEFAULT NULL,
p_Mensaje               VARCHAR2 DEFAULT 'Haga caso omiso a este mensaje',
p_Remitente             VARCHAR2 DEFAULT 'usuario@midominio',
p_Asunto                VARCHAR2 DEFAULT 'Este correo no tiene asunto',
p_TipoMensaje           VARCHAR2 DEFAULT 'TEXT',
p_Directorio            VARCHAR2 DEFAULT 'ATTACHMENTS', -- Directorio lógico por default
p_ArchivosAdjuntos      VARCHAR2
)
AS

-- ----------------------------------------------------------------------------------------------
-- Nombre            : P_SendMailAttach
-- Autor             : Ing. Rogelio "Wazu" Rodriguez
-- Descripcion       : Envio de correos utilizando el package UTL_SMTP
-- Requerimientos    : UTL_SMTP
-- Restricciones     :
-- Revisiones:
--   Fecha        Desarrollador                 Cambio
--   ===========  ========================      =================================================
--   03-APR-2012  Rogelio Wazu Rodriguez        Creación Inicial
--   30-JUL-2012  Rogelio Wazu Rodriguez        Se modifica en el procedure
                                                file_attach en content transfer
                                                sea después de Disposition
-- ----------------------------------------------------------------------------------------------

c utl_smtp.connection;

--Procedimiento para escritura de Headers
-----------------------------------------------------------------
PROCEDURE send_header(name IN VARCHAR2, header IN VARCHAR2) AS 
BEGIN 
utl_smtp.write_data(c, name || ': ' || header || utl_tcp.CRLF); 
END;
-----------------------------------------------------------------



--Procedimiento para Adición de Destinatarios
-----------------------------------------------------------------------------------------------------------------------------------
PROCEDURE add_rcpt(p_Destinatarios in VARCHAR2) AS
l_Cadena varchar2(500) := p_Destinatarios;
l_LargoCadena number;
l_Comas number;
l_PosicionComa number := 0;
l_Destinatario varchar2(100);
BEGIN

l_LargoCadena := length(l_Cadena);
l_Comas := l_LargoCadena-length(replace(l_Cadena,';'));

--Bloque 1 Asginación del RCPT
    IF l_Comas > 0 THEN
          FOR l_segmento IN 1 .. l_Comas LOOP
            
              l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, instr(l_Cadena,';',1,l_segmento) - (l_PosicionComa + 1));
            
              l_PosicionComa := instr(l_Cadena,';',1,l_Segmento);
              utl_smtp.rcpt(c, l_Destinatario);
              
          END LOOP;
    END IF;
-- Fin de Bloque 1

-- Bloque 2: Para inserción del ultimo recipient solicitado (o el primero, si es unico)
    l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, l_LargoCadena);
    utl_smtp.rcpt(c, l_Destinatario);
-- Fin Bloque 2  

END;
---------------------------------------------------------------------------------------------------------------------------------


-- Procedimiento de adición de cabeceras para destinatarios
---------------------------------------------------------------------------------------------------------------------------------
PROCEDURE add_headers_rcpt(p_Destinatarios IN VARCHAR2, p_Type IN VARCHAR2) AS
l_Cadena varchar2(500) := p_Destinatarios;
l_LargoCadena number;
l_Comas number;
l_PosicionComa number := 0;
l_Destinatario varchar2(100);
BEGIN

l_LargoCadena := length(l_Cadena);
l_Comas := l_LargoCadena-length(replace(l_Cadena,';'));

--Bloque 1 Asginación del Destinatario al header
    IF l_Comas > 0 THEN
          FOR l_segmento IN 1 .. l_Comas LOOP
            
              l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, instr(l_Cadena,';',1,l_segmento) - (l_PosicionComa + 1));
            
              l_PosicionComa := instr(l_Cadena,';',1,l_Segmento);
              
              -- Se generan los encabezados para envio, en caso de Bcc no se agrega el Header
              IF p_Type = 'TO' THEN
                send_header('To', l_Destinatario);
              ELSE
                send_header('Cc', l_Destinatario);
              END IF;
              
          END LOOP;
    END IF;
-- Fin de Bloque 1

-- Bloque 2: Para inserción del ultimo recipient solicitado (o el primero, si es unico)
    l_Destinatario := substr(l_Cadena, l_PosicionComa + 1, l_LargoCadena);
    IF p_Type = 'TO' THEN
       send_header('To', l_Destinatario);
    ELSE
       send_header('Cc', l_Destinatario);
    END IF;
-- Fin Bloque 2  

END;
----------------------------------------------------------------------------------------------------------------------------------------------


-- Procedimiento para adjuntar los archivos al stream del correo
---------------------------------------------------------------------------------------------------------------------
PROCEDURE file_attach(p_Archivo varchar2) AS
-- Variables para el procesamiento de Archivos
 rfile     RAW(57);
 flen      NUMBER;
 bsize     NUMBER;
 src_file  bfile;
 buffer_   integer := 57;
 i         integer := 1;
BEGIN

    -- Escribir cabecera MIME
    utl_smtp.write_data(c,'--MIME.Bound'||utl_tcp.CRLF);
    send_header('Content-Type','application/octet-stream; name="' || p_Archivo || '"');
    send_header('Content-Disposition', 'attachment; filename="' || p_Archivo || '"');
    send_header('Content-Transfer-Encoding', 'base64' );
    utl_smtp.write_data(c, utl_tcp.CRLF);
              
    -- Adición del Archivo 
    src_file := bfilename(p_Directorio, p_Archivo); 
              flen := dbms_lob.getlength(src_file);
              
              dbms_lob.fileopen(src_file, dbms_lob.file_readonly);
              
              while i < flen loop
                    dbms_lob.read( src_file, buffer_, i, rfile );
                    utl_smtp.write_raw_data(c, utl_encode.base64_encode(rfile));
                    utl_smtp.write_data(c, utl_tcp.CRLF);
                    i := i + buffer_;
              end loop while_loop;
              
              dbms_lob.fileclose(src_file);
              utl_smtp.write_data(c, utl_tcp.CRLF||utl_tcp.CRLF);

END;
------------------------------------------------------------------------------------------------------------------------------------


--Procedimiento para separación y Adición de Archivos Adjuntos
------------------------------------------------------------------------------------------------------------------------------------
PROCEDURE add_attachments(p_Adjuntos in VARCHAR2) AS
l_Cadena varchar2(500) := p_Adjuntos;
l_LargoCadena number;
l_Comas number;
l_PosicionComa number := 0;
l_Archivo varchar2(100);

-- Variables para el procesamiento de Archivos
 rfile     RAW(57);
 flen      NUMBER;
 bsize     NUMBER;
 src_file  bfile;
 buffer_   integer := 57;
 i         integer := 1;
BEGIN

l_LargoCadena := length(l_Cadena);
l_Comas := l_LargoCadena-length(replace(l_Cadena,';'));

--Bloque 1 Adición del Archivo
    IF l_Comas > 0 THEN
          FOR l_segmento IN 1 .. l_Comas LOOP
          
              l_Archivo := substr(l_Cadena, l_PosicionComa + 1, instr(l_Cadena,';',1,l_segmento) - (l_PosicionComa + 1));
              file_attach(l_Archivo);
            
              l_PosicionComa := instr(l_Cadena,';',1,l_Segmento);
              
          END LOOP;
    END IF;
-- Fin de Bloque 1

-- Bloque 2: Para inserción del ultimo archivo (o el primero, si es unico)
    l_Archivo := substr(l_Cadena, l_PosicionComa + 1, l_LargoCadena);
    file_attach(l_Archivo);
              
-- Fin Bloque 2  

END;
---------------------------------------------------------------------------------------------------------------------------------


-- Inicio del Programa Core para el correo
BEGIN 

    -- Bloque de Apertura de Conexión
    c := utl_smtp.open_connection('165.254.254.32',25);  --Recuerden que puede ser el nombre NetBios o la IP
    utl_smtp.helo(c, '165.254.254.32'); 
    utl_smtp.mail(c, p_Remitente);-- Remitente 

    -- Bloque de Adición de Destinatarios
    IF p_DestinatariosPara IS NOT NULL THEN
        add_rcpt(p_DestinatariosPara);
    END IF;

    IF p_DestinatariosCC IS NOT NULL THEN
        add_rcpt(p_DestinatariosCC);
    END IF;

    IF p_DestinatariosBcc IS NOT NULL THEN
        add_rcpt(p_DestinatariosBcc);
    END IF;

    --Bloque de Apertura de Datos
    utl_smtp.open_data(c); 

    -- Bloque de Adición de Cabeceras de Mail
    IF p_DestinatariosPara IS NOT NULL THEN
        add_headers_rcpt(p_DestinatariosPara, 'TO');
    END IF;

    IF p_DestinatariosCC IS NOT NULL THEN
        add_headers_rcpt(p_DestinatariosCC, 'CC');
    END IF;

    send_header('Subject', p_Asunto); --Asunto
    send_header('From', p_Remitente); -- De
    send_header('MIME-Version','1.0');
    send_header('Content-Type','multipart/mixed; boundary="MIME.Bound"');
    utl_smtp.write_data(c,'--MIME.Bound'||utl_tcp.CRLF);
    send_header('MIME-Version','1.0');
    
    IF p_Mensaje IS NOT NULL THEN
    
        IF p_TipoMensaje = 'TEXT' THEN
            send_header('MIME-Version','1.0');
            send_header('Content-Type','text/plain; charset=us-ascii');
            send_header('Content-Disposition', 'inline');
        ELSE
            send_header('MIME-Version','1.0');
            send_header('Content-Type', 'text/html;charset=windows-1252');
            send_header('Content-Disposition', 'inline');
            send_header('Content-Transfer_Encoding', '8bit');
        END IF;
    
    END IF;

    -- Bloque de Escritura del Mensaje 
    utl_smtp.write_data(c, utl_tcp.CRLF ||p_Mensaje||utl_tcp.CRLF);
    utl_smtp.write_data(c, utl_tcp.CRLF);
    
    -- Bloque de Envio de Adjuntos
    add_attachments(p_ArchivosAdjuntos);
    utl_smtp.write_data(c, utl_tcp.CRLF||'--MIME.Bound--'||utl_tcp.CRLF);

    -- Bloque de Cierre de Datos y Envio del Mail
    utl_smtp.close_data(c); 

    -- Bloque de Cierre de Conexión
    utl_smtp.quit(c); 

    EXCEPTION
     WHEN utl_smtp.Transient_Error OR utl_smtp.Permanent_Error then
        -- Bloque de Cierre de Conexión
        utl_smtp.quit(c); 
       raise_application_error(-20000, 'Unable to send mail: '||sqlerrm);
     WHEN OTHERS THEN
        -- Bloque de Cierre de Conexión
        utl_smtp.quit(c); 
        raise_application_error(-20000, 'Unable to send mail: '||sqlerrm);
    END;

END;
/ 

-- Ejemplo de uso

exec              P_SENDMAILATTACH(p_DestinatariosPara =>'usuario.uno@empresa1.com;usuario.dos@empresa2.com',
                                   p_DestinatariosCC => 'usuario@otraempresa.com',
                                   p_Remitente => 'InformesEmpresa1@empresa1.com',
                                   p_Mensaje =>'<h1> Informe </h1> <p>Cuerpo del Informe</p>',
                                   p_Asunto => 'Envío de Informe Empresa1',
                                   p_ArchivosAdjuntos => 'a6.pdf;a1.csv;A3.xml;A5.PDF;a6.txt',
                                   p_TipoMensaje => 'HTML');


Espero les sea de utilidad el código y les confirmo que funciona al 100%, cualquier pregunta o comentario, pueden dejarlo en este post.