Close

Sending Email with Oracle, Part 10: Pulling it all together

Sending Email with Oracle

In this series, parts 6 through 9 covered various MIME block and multipart types. Now we’ll put them all together. Taking all these steps may seem contrived but a structure such as this will likely be used by most modern email clients you may already be using.

In the code samples below the email constructed consists of a short message, a smiley face, and a small 2-column table of sample data. The data is also sent along in the form of a CSV file attachment. The message, face, and table will be presented in two alternative formats, HTML and plain text.

The HTML form of the email should render something like this:

Here’s your data, a CSV version of the content is in the attachment.



column1column2
data 1adata 1b
data 2adata 2b
data 3adata 3b
data 4adata 4b

The plain text form of the email should look something like this:

Here's your data, a CSV version of the content is in the attachment.

:)

column1   column2
data 1a   data 1b
data 2a   data 2b
data 3a   data 3b
data 4a   data 4b

Despite the simplicity of the final, rendered output, there is a lot going on in the nested MIME definitions. It can be difficult to read through them with common randomly generated boundaries. To aid in reading, I have constructed boundaries that help describe their scope. Before jumping into the full email body content lets look at an outline of the MIME nesting.

Since there is both a message and an attachment the outermost MIME time, that one defined in the email header is declared as multipart/mixed. The child blocks of this are the alternative block and the file attachment. If there was more than one file attached, they would all be at this same outer scope.

Within the alternative type there is text/plain block holding the plain text form of the message. The other alternative is yet another multipart layer of related content.

The root of the related content is an HTML block, with a related block for an embedded image content. The block scope hierarchy can be read as follows:

Content-Type: multipart/mixed; boundary="----OuterMixedBoundary"
------OuterMixedBoundary
    Content-Type: multipart/alternative; boundary="----SubBlockAlternativeBoundary"
    ------SubBlockAlternativeBoundary
        Content-Type: text/plain;
        ...
    ------SubBlockAlternativeBoundary
        Content-Type: multipart/related; boundary="----InnerRelatedBoundary"
        ------InnerRelatedBoundary
            Content-Type: text/html; 
            ...
        ------InnerRelatedBoundary
            Content-Type: image/jpeg; name="smiley.jpg"
            Content-Disposition: inline; filename="smiley.jpg"
            Content-Id: 
            Content-Transfer-Encoding: base64
            ...
        ------InnerRelatedBoundary--
    ------SubBlockAlternativeBoundary--
------OuterMixedBoundary
    Content-Type: text/plain; name="data.csv"
    Content-Disposition: attachment; filename="data.csv"
    ...
------OuterMixedBoundary--

For ease of illustration and reading, the code sample will send the entire MIME structure as a single, preconstructed varchar2 value. This mirrors the suggestion from earlier article of building all of the content before sending.

DECLARE
    v_connection   UTL_SMTP.connection;
BEGIN
    v_connection :=
        UTL_SMTP.open_connection(
            HOST                            => 'smtp.gmail.com',
            port                            => 587,
            wallet_path                     => 'file:/u01/app/oracle/wallets/gmail',
            -- password not needed because wallet is auto login
            secure_connection_before_smtp   => FALSE);

    UTL_SMTP.ehlo(v_connection, 'smtp.gmail.com');
    UTL_SMTP.starttls(v_connection);
    UTL_SMTP.auth(v_connection, 'sean.stuber@gmail.com', :password, 'PLAIN');

    UTL_SMTP.mail(v_connection, 'sean.stuber@gmail.com'); --from
    UTL_SMTP.rcpt(v_connection, 'sean.stuber@gmail.com'); --- to
    UTL_SMTP.open_data(v_connection);

    UTL_SMTP.write_data(v_connection, 'From: sean.stuber@gmail.com' || UTL_TCP.crlf);
    UTL_SMTP.write_data(v_connection, 'To: sean.stuber@gmail.com' || UTL_TCP.crlf);
    UTL_SMTP.write_data(v_connection, 'Subject: test all together' || UTL_TCP.crlf);
    UTL_SMTP.write_data(
        v_connection,
           'Content-Type: multipart/mixed; boundary="----OuterMixedBoundary"'
        || UTL_TCP.crlf);
    UTL_SMTP.write_data(v_connection, 'MIME-Version: 1.0' || UTL_TCP.crlf);
    UTL_SMTP.write_data(v_connection, UTL_TCP.crlf);

    UTL_SMTP.write_data(
        v_connection,
           q'[
------OuterMixedBoundary
Content-Type: multipart/alternative; boundary="----SubBlockAlternativeBoundary"

------SubBlockAlternativeBoundary
Content-Type: text/plain;

Here's your data, a CSV version of the content is in the attachment.

:)

column1   column2
data 1a   data 1b
data 2a   data 2b
data 3a   data 3b
data 4a   data 4b


------SubBlockAlternativeBoundary
Content-Type: multipart/related; boundary="----InnerRelatedBoundary"

------InnerRelatedBoundary
Content-Type: text/html; 

<html>
<head>
<style>
table, th, td {
  border: 1px solid black;
  border-collapse: collapse;   https://seanstuber.com/wp-content/uploads/2022/04/smiley-1.jpg
}
</style>
</head>
<body>
<h3>Here's your data, a CSV version of the content is in the attachment.</h3>
<img src="cid:smileyface">
<br>
<br>
<table style="width:25%")>
<tr style="background-color: #999966;"><th>column1</th><th>column2</th></tr>
<tr style="background-color: #f5f5ef;"><td>data 1a</td><td>data 1b</td></tr>
<tr style="background-color: #d8d8c0;"><td>data 2a</td><td>data 2b</td></tr>
<tr style="background-color: #f5f5ef;"><td>data 3a</td><td>data 3b</td></tr>
<tr style="background-color: #d8d8c0;"><td>data 4a</td><td>data 4b</td></tr>
</table>
</body>
</html>

------InnerRelatedBoundary
Content-Type: image/jpeg; name="smiley.jpg"
Content-Disposition: inline; filename="smiley.jpg"
Content-Id: <smileyface>
Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYH
BwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcI
DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAAR
CAAKAAoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA
AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK
FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG
h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl
5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA
AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk
NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE
hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk
5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9j/ihrd5/wkHjGS2+KGu6DbK2mQra21hY
yzaBPazpPLDbQyQtLcvqUUyQ4Ky4wvlbXYCvbaoX3hbS9T1W3v7nTbC4vrTHkXEtujyw4zja
xGVxk9D3NX6AP//Z

------InnerRelatedBoundary--

------SubBlockAlternativeBoundary--
------OuterMixedBoundary
Content-Type: text/plain; name="data.csv"
Content-Disposition: attachment; filename="data.csv"

"column1","column2"
"data 1a","data 1b"
"data 2a","data 2b"
"data 3a","data 3b"
"data 4a","data 4b"

------OuterMixedBoundary--]'
        || UTL_TCP.crlf);

    UTL_SMTP.close_data(v_connection);
    UTL_SMTP.quit(v_connection);
EXCEPTION
    WHEN OTHERS
    THEN
        DBMS_OUTPUT.put_line(DBMS_UTILITY.format_error_stack);
END;

For relatively short content I might try to build the content serially to create the entire body. More likely though, especially as the content gets large and with more blocks I would prefer to build it through a series of nested procedure or function calls. Each parent procedure defining a boundary and block header and then calling child routines to fill in the content, delimiting them as I went.

An outline of code to build the MIME content above might look something like this:

DECLARE
    v_connection   UTL_SMTP.connection;
    v_body         VARCHAR2(32767);

    PROCEDURE start_email(p_connection OUT UTL_SMTP.connection)
    IS
    BEGIN
        p_connection :=
            UTL_SMTP.open_connection(
                HOST                            => 'smtp.gmail.com',
                port                            => 587,
                wallet_path                     => 'file:/u01/app/oracle/wallets/gmail',
                -- password not needed because wallet is auto login
                secure_connection_before_smtp   => FALSE);

        UTL_SMTP.ehlo(p_connection, 'smtp.gmail.com');
        UTL_SMTP.starttls(p_connection);
        UTL_SMTP.auth(p_connection,
                      'sean.stuber@gmail.com',
                      :password,
                      'PLAIN');

        UTL_SMTP.mail(p_connection, 'sean.stuber@gmail.com'); --from
        UTL_SMTP.rcpt(p_connection, 'sean.stuber@gmail.com'); --- to
        UTL_SMTP.open_data(p_connection);

        UTL_SMTP.write_data(p_connection,
                            'From: sean.stuber@gmail.com' || UTL_TCP.crlf);
        UTL_SMTP.write_data(p_connection,
                            'To: sean.stuber@gmail.com' || UTL_TCP.crlf);
        UTL_SMTP.write_data(p_connection,
                            'Subject: test all together 2' || UTL_TCP.crlf);
        UTL_SMTP.write_data(p_connection, 'MIME-Version: 1.0' || UTL_TCP.crlf);
    END;

    FUNCTION plain_text_content
        RETURN VARCHAR2
    IS
    BEGIN
        RETURN q'[Content-Type: text/plain;

Here's your data, a CSV version of the content is in the attachment.

:)

column1   column2
data 1a   data 1b
data 2a   data 2b
data 3a   data 3b
data 4a   data 4b
]';
    END;

    FUNCTION html_content
        RETURN VARCHAR2
    IS
    BEGIN
        RETURN q'[Content-Type: text/html; 

<html>
<head>
<style>
table, th, td {
  border: 1px solid black;
  border-collapse: collapse;   https://seanstuber.com/wp-content/uploads/2022/04/smiley-1.jpg
}
</style>
</head>
<body>
<h3>Here's your data, a CSV version of the content is in the attachment.</h3>
<img src="cid:smileyface">
<br>
<br>
<table style="width:25%")>
<tr style="background-color: #999966;"><th>column1</th><th>column2</th></tr>
<tr style="background-color: #f5f5ef;"><td>data 1a</td><td>data 1b</td></tr>
<tr style="background-color: #d8d8c0;"><td>data 2a</td><td>data 2b</td></tr>
<tr style="background-color: #f5f5ef;"><td>data 3a</td><td>data 3b</td></tr>
<tr style="background-color: #d8d8c0;"><td>data 4a</td><td>data 4b</td></tr>
</table>
</body>
</html>
]';
    END;

    FUNCTION embedded_image
        RETURN VARCHAR2
    IS
    BEGIN
        RETURN q'[Content-Type: image/jpeg; name="smiley.jpg"
Content-Disposition: inline; filename="smiley.jpg"
Content-Id: <smileyface>
Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYH
BwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcI
DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAAR
CAAKAAoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA
AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK
FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG
h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl
5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA
AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk
NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE
hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk
5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9j/ihrd5/wkHjGS2+KGu6DbK2mQra21hY
yzaBPazpPLDbQyQtLcvqUUyQ4Ky4wvlbXYCvbaoX3hbS9T1W3v7nTbC4vrTHkXEtujyw4zja
xGVxk9D3NX6AP//Z
]';
    END;

    FUNCTION file_attachment
        RETURN VARCHAR2
    IS
    BEGIN
        RETURN q'[Content-Type: text/plain; name="data.csv"
Content-Disposition: attachment; filename="data.csv"

"column1","column2"
"data 1a","data 1b"
"data 2a","data 2b"
"data 3a","data 3b"
"data 4a","data 4b"
]';
    END;

    FUNCTION build_related_content
        RETURN VARCHAR2
    IS
        c_boundary   VARCHAR2(35) := '----InnerRelatedBoundary';
        v_output     VARCHAR2(32767);
    BEGIN
        v_output :=
            'Content-Type: multipart/related; boundary="' || c_boundary || '"';
        v_output := v_output || UTL_TCP.crlf;
        v_output := v_output || UTL_TCP.crlf;
        v_output := v_output || '--' || c_boundary || UTL_TCP.crlf;
        v_output := v_output || html_content;
        v_output := v_output || '--' || c_boundary || UTL_TCP.crlf;
        v_output := v_output || embedded_image;
        v_output := v_output || '--' || c_boundary || '--' || UTL_TCP.crlf;
        RETURN v_output;
    END;

    FUNCTION build_alternative_content
        RETURN VARCHAR2
    IS
        c_boundary   VARCHAR2(35) := '----SubBlockAlternativeBoundary';
        v_output     VARCHAR2(32767);
    BEGIN
        v_output :=
               'Content-Type: multipart/alternative; boundary="'
            || c_boundary
            || '"';
        v_output := v_output || UTL_TCP.crlf;
        v_output := v_output || UTL_TCP.crlf;
        v_output := v_output || '--' || c_boundary || UTL_TCP.crlf;
        v_output := v_output || plain_text_content;
        v_output := v_output || '--' || c_boundary || UTL_TCP.crlf;
        v_output := v_output || build_related_content;
        v_output := v_output || '--' || c_boundary || '--' || UTL_TCP.crlf;
        RETURN v_output;
    END;

    FUNCTION build_mixed_content
        RETURN VARCHAR2
    IS
        c_boundary   VARCHAR2(35) := '----OuterMixedBoundary';
        v_output     VARCHAR2(32767);
    BEGIN
        v_output :=
            'Content-Type: multipart/mixed; boundary="' || c_boundary || '"';
        v_output := v_output || UTL_TCP.crlf;
        v_output := v_output || UTL_TCP.crlf;
        v_output := v_output || '--' || c_boundary || UTL_TCP.crlf;
        v_output := v_output || build_alternative_content;
        v_output := v_output || '--' || c_boundary || UTL_TCP.crlf;
        v_output := v_output || file_attachment;
        v_output := v_output || '--' || c_boundary || '--' || UTL_TCP.crlf;
        RETURN v_output;
    END;
BEGIN
    v_body := build_mixed_content;

    start_email(v_connection);

    UTL_SMTP.write_data(v_connection, v_body || UTL_TCP.crlf);

    UTL_SMTP.close_data(v_connection);
    UTL_SMTP.quit(v_connection);
EXCEPTION
    WHEN OTHERS
    THEN
        DBMS_OUTPUT.put_line(DBMS_UTILITY.format_error_stack);
END;

Before connecting to the SMTP server I build the email body. At the outer most layer is the mixed content. It then calls the alternative and attachment routines. The alternative function calls the plain text and related content. In the last layer of nesting, the related function calls the html and embedded image routines. Each routine is then responsible for its own content with the parent delimiting them as needed.

I hope you found this example helpful. I use it as a reference myself.
Thank you for reading. Questions and comments, as always, are welcome.