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.
column1 | column2 |
---|---|
data 1a | data 1b |
data 2a | data 2b |
data 3a | data 3b |
data 4a | data 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.