Generic Dynamic PL/SQL Send Mail Procedure with Dynamic Query, Body and CSV Attachment


Generic Dynamic Mail PL/SQL Procedure with Dynamic Query, Body and CSV Attachment

CREATE OR REPLACE PROCEDURE FMS_ADMIN.SEND_DYNAMIC_MAIL_WITH_ATTACHMENT
(
    P_TO_MAIL        IN VARCHAR2,
    P_FROM_MAIL      IN VARCHAR2,
    P_SUBJECT        IN VARCHAR2,
    P_BODY           IN CLOB,
    P_QUERY          IN CLOB,
    P_FILE_NAME      IN VARCHAR2 DEFAULT 'attachment.csv',
    P_SMTP_HOST      IN VARCHAR2 DEFAULT '10.195.255.255',
    P_SMTP_PORT      IN NUMBER   DEFAULT 123
)
IS

    ------------------------------------------------------------------
    -- SMTP VARIABLES
    ------------------------------------------------------------------

    L_MAIL_CONN      UTL_SMTP.CONNECTION;

    L_BOUNDARY       VARCHAR2(100) := '----=*#abc1234321#*=';

    ------------------------------------------------------------------
    -- CSV VARIABLES
    ------------------------------------------------------------------

    L_CLOB           CLOB;

    L_LEN            NUMBER;

    L_POS            NUMBER := 1;

    L_STEP           NUMBER := 1900;

    ------------------------------------------------------------------
    -- DBMS_SQL VARIABLES
    ------------------------------------------------------------------

    L_CURSOR         INTEGER;

    L_COL_CNT        NUMBER;

    L_DESC_TAB       DBMS_SQL.DESC_TAB;

    L_COLUMN_VALUE   VARCHAR2(4000);

    L_STATUS         NUMBER;

    L_HEADER         VARCHAR2(32767);

    L_LINE           VARCHAR2(32767);

BEGIN

    ------------------------------------------------------------------
    -- CREATE TEMP CLOB
    ------------------------------------------------------------------

    DBMS_LOB.CREATETEMPORARY(L_CLOB, TRUE);

    ------------------------------------------------------------------
    -- OPEN CURSOR
    ------------------------------------------------------------------

    L_CURSOR := DBMS_SQL.OPEN_CURSOR;

    DBMS_SQL.PARSE
    (
        L_CURSOR,
        P_QUERY,
        DBMS_SQL.NATIVE
    );

    ------------------------------------------------------------------
    -- DESCRIBE COLUMNS
    ------------------------------------------------------------------

    DBMS_SQL.DESCRIBE_COLUMNS
    (
        L_CURSOR,
        L_COL_CNT,
        L_DESC_TAB
    );

    ------------------------------------------------------------------
    -- DEFINE COLUMNS
    ------------------------------------------------------------------

    FOR I IN 1 .. L_COL_CNT
    LOOP

        DBMS_SQL.DEFINE_COLUMN
        (
            L_CURSOR,
            I,
            L_COLUMN_VALUE,
            4000
        );

        IF I > 1 THEN
            L_HEADER := L_HEADER || ',';
        END IF;

        L_HEADER := L_HEADER || L_DESC_TAB(I).COL_NAME;

    END LOOP;

    ------------------------------------------------------------------
    -- WRITE CSV HEADER
    ------------------------------------------------------------------

    DBMS_LOB.WRITEAPPEND
    (
        L_CLOB,
        LENGTH(L_HEADER || CHR(10)),
        L_HEADER || CHR(10)
    );

    ------------------------------------------------------------------
    -- EXECUTE QUERY
    ------------------------------------------------------------------

    L_STATUS := DBMS_SQL.EXECUTE(L_CURSOR);

    ------------------------------------------------------------------
    -- FETCH ROWS
    ------------------------------------------------------------------

    WHILE DBMS_SQL.FETCH_ROWS(L_CURSOR) > 0
    LOOP

        L_LINE := NULL;

        FOR I IN 1 .. L_COL_CNT
        LOOP

            DBMS_SQL.COLUMN_VALUE
            (
                L_CURSOR,
                I,
                L_COLUMN_VALUE
            );

            ------------------------------------------------------------------
            -- HANDLE SPECIAL CHARACTERS
            ------------------------------------------------------------------

            L_COLUMN_VALUE := REPLACE(L_COLUMN_VALUE, '"', '""');

            IF I > 1 THEN
                L_LINE := L_LINE || ',';
            END IF;

            L_LINE := L_LINE || '"' || NVL(L_COLUMN_VALUE, '') || '"';

        END LOOP;

        DBMS_LOB.WRITEAPPEND
        (
            L_CLOB,
            LENGTH(L_LINE || CHR(10)),
            L_LINE || CHR(10)
        );

    END LOOP;

    ------------------------------------------------------------------
    -- CLOSE CURSOR
    ------------------------------------------------------------------

    DBMS_SQL.CLOSE_CURSOR(L_CURSOR);

    ------------------------------------------------------------------
    -- OPEN SMTP CONNECTION
    ------------------------------------------------------------------

    L_MAIL_CONN := UTL_SMTP.OPEN_CONNECTION
                   (
                       P_SMTP_HOST,
                       P_SMTP_PORT
                   );

    UTL_SMTP.HELO
    (
        L_MAIL_CONN,
        P_SMTP_HOST
    );

    ------------------------------------------------------------------
    -- MAIL FROM
    ------------------------------------------------------------------

    UTL_SMTP.MAIL
    (
        L_MAIL_CONN,
        P_FROM_MAIL
    );

    ------------------------------------------------------------------
    -- MULTIPLE RECIPIENTS
    ------------------------------------------------------------------

    FOR X IN
    (
        SELECT REGEXP_SUBSTR
               (
                   P_TO_MAIL,
                   '[^,;]+',
                   1,
                   LEVEL
               ) EMAIL_ID
        FROM DUAL
        CONNECT BY REGEXP_SUBSTR
                   (
                       P_TO_MAIL,
                       '[^,;]+',
                       1,
                       LEVEL
                   ) IS NOT NULL
    )
    LOOP

        UTL_SMTP.RCPT
        (
            L_MAIL_CONN,
            TRIM(X.EMAIL_ID)
        );

    END LOOP;

    ------------------------------------------------------------------
    -- OPEN DATA
    ------------------------------------------------------------------

    UTL_SMTP.OPEN_DATA(L_MAIL_CONN);

    ------------------------------------------------------------------
    -- MAIL HEADERS
    ------------------------------------------------------------------

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'Date: ' ||
        TO_CHAR(SYSDATE,'DD-MON-YYYY HH24:MI:SS') ||
        UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'To: ' || P_TO_MAIL || UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'From: ' || P_FROM_MAIL || UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'Subject: ' || P_SUBJECT || UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'MIME-Version: 1.0' ||
        UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'Content-Type: multipart/mixed; boundary="' ||
        L_BOUNDARY || '"' ||
        UTL_TCP.CRLF ||
        UTL_TCP.CRLF
    );

    ------------------------------------------------------------------
    -- HTML BODY PART
    ------------------------------------------------------------------

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        '--' || L_BOUNDARY || UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'Content-Type: text/html; charset=UTF-8' ||
        UTL_TCP.CRLF ||
        UTL_TCP.CRLF
    );

    ------------------------------------------------------------------
    -- DYNAMIC HTML BODY
    ------------------------------------------------------------------

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,

        '<html>' ||
        '<body style="font-family:Arial;">' ||

        '<table width="100%" cellpadding="5" cellspacing="0" ' ||
        'style="border-bottom:2px solid #004A99;">' ||

        '<tr>' ||

        '<td width="120">' ||

        '<img src="https://upload.wikimedia.org/wikipedia/en/thumb/3/3e/BSNL_Logo.svg/1200px-BSNL_Logo.svg.png" ' ||
        'width="100" height="100">' ||

        '</td>' ||

        '<td>' ||

        '<h2 style="color:#004A99;">BSNL Franchise Management System</h2>' ||

        '<p style="font-size:14px;color:#333333;">' ||
        'Automated Notification from FMS' ||
        '</p>' ||

        '</td>' ||

        '</tr>' ||

        '</table>' ||

        '<br>' ||

        P_BODY ||

        '<br><br>' ||

        '<table border="1" cellpadding="5" cellspacing="0" ' ||
        'style="border-collapse:collapse;font-size:13px;">' ||

        '<tr style="background-color:#004A99;color:white;">' ||
        '<th>Date</th>' ||
        '<th>Attachment</th>' ||
        '</tr>' ||

        '<tr>' ||
        '<td>' || TO_CHAR(SYSDATE,'DD-MON-YYYY HH24:MI:SS') || '</td>' ||
        '<td>' || P_FILE_NAME || '</td>' ||
        '</tr>' ||

        '</table>' ||

        '<br><br>' ||

        '<p style="font-size:12px;color:gray;">' ||
        'This is a system generated email from BSNL FMS.' ||
        '</p>' ||

        '</body>' ||
        '</html>' ||

        UTL_TCP.CRLF ||
        UTL_TCP.CRLF
    );

    ------------------------------------------------------------------
    -- ATTACHMENT HEADER
    ------------------------------------------------------------------

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        '--' || L_BOUNDARY || UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'Content-Type: text/csv; name="' ||
        P_FILE_NAME || '"' ||
        UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'Content-Disposition: attachment; filename="' ||
        P_FILE_NAME || '"' ||
        UTL_TCP.CRLF
    );

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        'Content-Transfer-Encoding: 8bit' ||
        UTL_TCP.CRLF ||
        UTL_TCP.CRLF
    );

    ------------------------------------------------------------------
    -- WRITE CSV DATA
    ------------------------------------------------------------------

    L_LEN := DBMS_LOB.GETLENGTH(L_CLOB);

    WHILE L_POS <= L_LEN
    LOOP

        UTL_SMTP.WRITE_DATA
        (
            L_MAIL_CONN,
            DBMS_LOB.SUBSTR
            (
                L_CLOB,
                L_STEP,
                L_POS
            )
        );

        L_POS := L_POS + L_STEP;

    END LOOP;

    ------------------------------------------------------------------
    -- END MIME
    ------------------------------------------------------------------

    UTL_SMTP.WRITE_DATA
    (
        L_MAIL_CONN,
        UTL_TCP.CRLF ||
        '--' || L_BOUNDARY || '--' ||
        UTL_TCP.CRLF
    );

    ------------------------------------------------------------------
    -- CLOSE MAIL
    ------------------------------------------------------------------

    UTL_SMTP.CLOSE_DATA(L_MAIL_CONN);

    UTL_SMTP.QUIT(L_MAIL_CONN);

    ------------------------------------------------------------------
    -- FREE TEMP CLOB
    ------------------------------------------------------------------

    IF DBMS_LOB.ISTEMPORARY(L_CLOB) = 1 THEN
        DBMS_LOB.FREETEMPORARY(L_CLOB);
    END IF;

EXCEPTION

    WHEN OTHERS THEN

        ------------------------------------------------------------------
        -- CLOSE SMTP CONNECTION
        ------------------------------------------------------------------

        BEGIN
            UTL_SMTP.QUIT(L_MAIL_CONN);
        EXCEPTION
            WHEN OTHERS THEN
                NULL;
        END;

        ------------------------------------------------------------------
        -- CLOSE CURSOR
        ------------------------------------------------------------------

        BEGIN
            IF DBMS_SQL.IS_OPEN(L_CURSOR) THEN
                DBMS_SQL.CLOSE_CURSOR(L_CURSOR);
            END IF;
        EXCEPTION
            WHEN OTHERS THEN
                NULL;
        END;

        ------------------------------------------------------------------
        -- FREE CLOB
        ------------------------------------------------------------------

        BEGIN
            IF DBMS_LOB.ISTEMPORARY(L_CLOB) = 1 THEN
                DBMS_LOB.FREETEMPORARY(L_CLOB);
            END IF;
        EXCEPTION
            WHEN OTHERS THEN
                NULL;
        END;

        RAISE_APPLICATION_ERROR
        (
            -20001,
            'Error in SEND_DYNAMIC_MAIL_WITH_ATTACHMENT : ' || SQLERRM
        );

END;
/

Sample Usage

BEGIN

    FMS_ADMIN.SEND_DYNAMIC_MAIL_WITH_ATTACHMENT
    (
        P_TO_MAIL   => 'abc@bsnl.co.in,xyz@bsnl.co.in',

        P_FROM_MAIL => 'bsnlfms@cdr.bsnl.co.in',

        P_SUBJECT   => 'Skypro Voluntary Closures',

        P_BODY      =>
            '<p>Please find attached voluntary closure details.</p>' ||
            '<p>Kindly process all disconnections immediately.</p>',

        P_QUERY     =>
            'SELECT
                 ORDER_NO,
                 CIRCLE,
                 PHONE_NO,
                 CUST_ACCNT_NO,
                 MOBILE,
                 SUBSCRIBER_ID
             FROM IFTV_VOLUNTARY_CLOSURES
             WHERE SENT_TO_SKYPRO IS NULL',

        P_FILE_NAME => 'skypro_closures.csv'
    );

END;
/

Features

  • Fully dynamic SQL query

  • Automatically generates CSV headers

  • Dynamic HTML email body

  • Supports multiple recipients

  • Generic reusable utility procedure

  • Handles any table/query

  • Proper exception handling

  • CSV special character handling

  • Dynamic attachment filename

  • SMTP configurable

  • HTML formatted mail

  • No hardcoded columns


The procedure SEND_DYNAMIC_MAIL_WITH_ATTACHMENT is a generic Oracle PL/SQL utility to:

  • Execute any SQL query dynamically

  • Convert query output into CSV format

  • Attach the CSV to an email

  • Send a formatted HTML email using SMTP

It is designed as a reusable framework instead of writing separate mail procedures for each report.


Procedure Purpose

This procedure automates report mailing from Oracle database.

Typical use cases:

  • Daily reports

  • Fault reports

  • Invoice reports

  • Pending orders

  • Customer lists

  • Audit extracts

  • Exception reports


Input Parameters

ParameterPurpose
P_TO_MAILRecipient email IDs
P_FROM_MAILSender email ID
P_SUBJECTEmail subject
P_BODYDynamic HTML/text body
P_QUERYSQL query whose output becomes CSV
P_FILE_NAMEAttachment filename
P_SMTP_HOSTSMTP server IP
P_SMTP_PORTSMTP port

High Level Flow

INPUT QUERY
     ↓
EXECUTE QUERY DYNAMICALLY
     ↓
FETCH COLUMN NAMES
     ↓
CREATE CSV HEADER
     ↓
FETCH ROWS
     ↓
GENERATE CSV CONTENT
     ↓
OPEN SMTP CONNECTION
     ↓
BUILD HTML EMAIL
     ↓
ATTACH CSV
     ↓
SEND EMAIL
     ↓
CLEANUP

Step-by-Step Explanation


1. SMTP Variables

L_MAIL_CONN UTL_SMTP.CONNECTION;

Stores SMTP session connection.


2. MIME Boundary

L_BOUNDARY VARCHAR2(100) := '----=*#abc1234321#*=';

Used to separate:

  • HTML body

  • Attachment

inside one email.

Email internally looks like:

BODY PART
---------
ATTACHMENT PART

Boundary marks those sections.


3. Temporary CLOB

L_CLOB CLOB;

Stores entire CSV content.

Because report size can be large.


4. Dynamic SQL Engine

DBMS_SQL

This is the most important part.

It allows execution of dynamic queries.

Example:

SELECT * FROM EMP

or

SELECT ORDER_NO, PHONE_NO FROM XYZ

without hardcoding columns.


5. Parse Dynamic Query

DBMS_SQL.PARSE
(
    L_CURSOR,
    P_QUERY,
    DBMS_SQL.NATIVE
);

Oracle parses the query supplied in parameter.


6. Describe Columns Dynamically

DBMS_SQL.DESCRIBE_COLUMNS

This reads:

  • number of columns

  • column names

  • datatype metadata

Example:

ORDER_NO
PHONE_NO
STATUS

This is how CSV headers are generated automatically.


7. Define Columns

DBMS_SQL.DEFINE_COLUMN

Allocates memory for fetching column values.

Without this, Oracle cannot retrieve row values.


8. Generate CSV Header

L_HEADER := L_HEADER || L_DESC_TAB(I).COL_NAME;

Builds:

ORDER_NO,PHONE_NO,STATUS

automatically.


9. Execute Query

L_STATUS := DBMS_SQL.EXECUTE(L_CURSOR);

Runs the SQL query.


10. Fetch Rows

WHILE DBMS_SQL.FETCH_ROWS(L_CURSOR) > 0

Reads one row at a time.


11. Fetch Column Values

DBMS_SQL.COLUMN_VALUE

Gets each column value dynamically.

Example:

1001
9848012345
ACTIVE

12. CSV Special Character Handling

REPLACE(L_COLUMN_VALUE, '"', '""')

Escapes double quotes.

Important for valid CSV format.

Example:

A "TEST"

becomes

"A ""TEST"""

13. Build CSV Line

L_LINE := L_LINE || '"' || VALUE || '"';

Creates safe CSV rows.

Example:

"1001","9848012345","ACTIVE"

14. Write to CLOB

DBMS_LOB.WRITEAPPEND

Appends data into large CSV memory object.


15. SMTP Connection

UTL_SMTP.OPEN_CONNECTION

Connects to mail server.

Example:

10.195.216.57:587

16. Multiple Recipients

REGEXP_SUBSTR

Splits:

a@x.com,b@y.com

into separate email IDs.


17. MIME Email Headers

Content-Type: multipart/mixed

Tells mail client:

This email contains attachment(s)


18. HTML Body

The procedure creates professional HTML mail.

Includes:

  • BSNL header

  • Logo

  • Dynamic message

  • Attachment details

  • Footer


19. Attachment Section

Content-Disposition: attachment

Tells email client:

Download this as file


20. Write CSV Attachment

DBMS_LOB.SUBSTR

Sends CSV in chunks.

Necessary because SMTP cannot send huge data in one write.


21. End MIME

--boundary--

Marks end of email content.


22. Cleanup

The procedure safely closes:

  • SMTP connection

  • Cursor

  • Temporary CLOB

Prevents memory leaks.


23. Exception Handling

WHEN OTHERS

Ensures cleanup happens even if:

  • SMTP fails

  • Query fails

  • Network issue occurs

Then raises readable error.


Why This Procedure Is Powerful

Earlier Approach

You had:

  • one procedure per report

  • hardcoded columns

  • hardcoded table

  • hardcoded CSV logic

Very difficult to maintain.


New Generic Approach

Now only this changes:

P_QUERY
P_BODY
P_SUBJECT
P_FILE_NAME

Everything else is reusable.


Example

Input Query

SELECT EMPNO, ENAME, SAL
FROM EMP

Automatically Generated CSV

EMPNO,ENAME,SAL
7369,SMITH,800
7499,ALLEN,1600

Advantages

FeatureBenefit
Dynamic queryReusable
Auto CSV headersNo coding needed
HTML mailProfessional
Generic utilitySingle framework
Multiple recipientsFlexible
CLOB handlingLarge reports
Exception handlingStable
CSV escapingCorrect format

Recommended Enhancements

Future improvements possible:

  • ZIP attachment

  • XLSX attachment

  • CC/BCC support

  • Inline table preview

  • Password protected attachment

  • Scheduler integration

  • Logging table

  • Retry mechanism

  • SMTP authentication

  • TLS/SSL support

Post a Comment

And that's all there is to it!

If anyone has any other questions or requests for future How To posts, you can either ask them in the comments or email me. Please don't feel shy at all!

I'm certainly not an expert, but I'll try my hardest to explain what I do know and research what I don't know.

Previous Post Next Post