Sending Email via the Database – Using utl_smtp By Bradley D. Brown What is SMTP? SMTP stands for SIMPLE MAIL TRANSFER PROTOCOL. This is the protocol that was developed to allow people around the world to exchange electronic mail. In other words, it’s the behind the scenes protocol that handles this for us. What is utl_smtp? Oracle provides a collection of database packages to simplify our database development using Oracle products. Utl stands for utility and you know what SMTP stands for. In other words, this is an email utility that provides us with the ability to email from the database. Now this is powerful stuff! In other words, you can dynamically generate email from the database and you can dynamically send it to different people based on different criteria. Intelligent email capabilities galore. The email that you send can be sent as a standard ASCII text email or an enhanced HTML email. Most anything is possible! This package provides SMTP client-side access functionality to PL/SQL programs so they can send electronic mails via SMTP. It does not allow the PL/SQL program to receive e-mails. The SMTP protocol is defined in RFC 821 and RFC 1869 – to get more information on this protocol, you can search for these RFCs. Utl_smtp provides an API directly to the SMTP protocol. Why both Procedures and Functions? You may notice that most of the SMTP API has a function form and a procedure form. You might ask, why is this? The function form returns the reply message after the command is sent. The reply message is in the form of "XXX <an optional reply message>", where XXX is the reply code. The procedure form of the same API calls the function form of the API, checks the reply code and raises transient_error or permanent_error exception if the reply code is in the 400 or 500 range. The function form of the API does not raise either of the two exceptions. In other words, it’s up to you to process the reply code accordingly. Exceptions There are three different exceptions that can be generated by any of the procedure/function calls. They are: Call Sequence When connecting to an SMTP server, you must follow a pretty standard call sequence – in other words, the order in which things are processed. An SMTP connection is initiated by a call to open_connection, which returns a SMTP connection with the SMTP server. After a connection is established, the following procedure/function calls (of utl_smtp) are required to send a mail: helo() or ehlo() - identify the domain of the sender mail() - start a mail, specify the sender rcpt() - specify the recipient open_data() - start the mail body write_data() - write the mail header/body (multiple calls) close_data() - close the mail body and send the mail quit() - close the SMTP connection invalid_operation - Operation is invalid or specified in the wrong call sequence - such as calling API other than write_data(), write_raw_data() or close_data() after open_data() is called or calling write_data(), write_raw_data() or close_data() without first calling open_data() transient_error - Transient server error in 400 range permanent_error Permanent server error in 500 range Reply Codes As mentioned, for each call to the SMTP, a reply code will be passed back to your program unit. The 200-300 codes are really just informational messages, not errors. The 400-500 codes are error messages. The following list contains each of the reply codes that could be returned: 211 - System status, or system help reply 214 - Help message - [Information on how to use the receiver or the meaning of a particular nonstandard command; this reply is useful only to the human user] 220 - <domain> Service ready 221 - <domain> Service closing transmission channel 250 - Requested mail action okay, completed 251 - User not local; will forward to <forward-path> 354 - Start mail input; end with <CRLF>.<CRLF> 421 - <domain> Service not available, closing transmission channel [This may be a reply to any command if the service knows it must shut down] 450 Requested mail action not taken: mailbox unavailable [E.g., mailbox busy] 451 - Requested action aborted: local error in processing 452 - Requested action not taken: insufficient system storage 500 - Syntax error, command unrecognized - [This may include errors such as command line too long] 501 - Syntax error in parameters or arguments 502 - Command not implemented 503 - Bad sequence of commands 504 - Command parameter not implemented 550 - Requested action not taken: mailbox unavailable [E.g., mailbox not found, no access] 551 - User not local; please try <forward-path> 552 - Requested mail action aborted: exceeded storage allocation 553 - Requested action not taken: mailbox name not allowed - [E.g., mailbox syntax incorrect] 554 - Transaction failed Example 1 – Send Hello World! Enough chatter about SMTP, let’s look at some actual code that will simply send an email to a user via the utl_smtp API: DECLARE c utl_smtp.connection; PROCEDURE send_header(name IN VARCHAR2, header IN VARCHAR2) AS BEGIN utl_smtp.write_data(c, name || ': ' || header || utl_tcp.CRLF); END; BEGIN c := utl_smtp.open_connection('smtpserver.tusc.com'); utl_smtp.helo(c, ‘tusc.com'); utl_smtp.mail(c, 'sender@tusc.com'); utl_smtp.rcpt(c, 'recipient@wherever.com'); utl_smtp.open_data(c); send_header('From', '"Sender" <sender@tusc.com>'); send_header('To', '"Recipient" <recipient@wherever.com>'); send_header('To', '"Recipient" <recipient@wherever.com>'); send_header('Subject', 'Hello'); utl_smtp.write_data(c, utl_tcp.CRLF || 'Hello, world!'); utl_smtp.close_data(c); utl_smtp.quit(c); EXCEPTION WHEN utl_smtp.transient_error OR utl_smtp.permanent_error THEN utl_smtp.quit(c); raise_application_error(-20000, 'Failed to send mail due to the following error: ' || sqlerrm); END; Example 2 - Send a Message from Anyone to Anyone This next example shows a generic procedure that will send mail from anyone to anyone. All of the parameters are passed into the procedure. This could be a very useful procedure for you: procedure send_mail (in_mail_server varchar2 default 'smtp.ix.netcom.com', in_sender_email varchar2 default ‘me@tusc.com', in_sender_name varchar2 default ‘My Full Name', in_recipient_email varchar2 default ‘you@tusc.com', in_recipient_name varchar2 default ‘Your Full Name', in_html_flg varchar2 default 'N', in_subject varchar2 default 'No Subject was Provided', in_importance varchar2 default 'Normal', in_body varchar2 default 'No Body for this Email‘ ) as -- Open the Connection, Set Mime Type nbt_connection utl_smtp.connection; nbt_message varchar2(30000); BEGIN nbt_connection := utl_smtp.open_connection(in_mail_server); utl_smtp.helo(nbt_connection, in_mail_server); utl_smtp.mail(nbt_connection, in_sender_email); utl_smtp.rcpt(nbt_connection, in_recipient_email); utl_smtp.open_data(nbt_connection ); if in_html_flg != 'N' then -- Content-Type: text/html utl_smtp.write_data(nbt_connection, 'Content-Type: text/html' || utl_tcp.CRLF); end if; -- Set Header Info (From, To, Subject) nbt_message := 'From: ' || '"' || in_sender_name || '" <' || in_sender_email || '>'; utl_smtp.write_data(nbt_connection, nbt_message || utl_tcp.CRLF); nbt_message := 'To: ' || '"' || in_recipient_name || '" <' || in_recipient_email || '>'; utl_smtp.write_data(nbt_connection, nbt_message || utl_tcp.CRLF); nbt_message := 'Subject: ' || in_subject; utl_smtp.write_data(nbt_connection, nbt_message || utl_tcp.CRLF); -- Set Importance, Write Body if in_importance != 'Normal' then -Normal is the default nbt_message := 'Importance: ' || in_importance; utl_smtp.write_data(nbt_connection, nbt_message || utl_tcp.CRLF); end if; -- Write the body, but first end the header if length(in_body) <= 2000 then utl_smtp.write_data(nbt_connection, utl_tcp.CRLF || in_body); else utl_smtp.write_data(nbt_connection, utl_tcp.CRLF); for i in 1 round(length(in_body)/2000) loop utl_smtp.write_data(nbt_connection, .. substr(in_body, ((i1)*2000)+1, 2000)); end loop; end if; -- Wrap up and Error Processing utl_smtp.close_data(nbt_connectio n); utl_smtp.quit(nbt_connection); EXCEPTION WHEN others then utl_smtp.transient_error OR utl_smtp.permanent_error THEN utl_smtp.quit(nbt_connection); error_routine('Failed to send mail due to the following error:' || sqlerrm); END; Example 3 - Pulling Data from Database Using the above procedure to send an email message, we are going to dynamically pull information (a report) from the database to generate an HTML email message. Notice that we use the htf package to build the HTML. After building the list of employees, we send the report to each of the employees on the list: procedure email_employee_list as nbt_body varchar2(30000) := ‘’; cursor emp_cur is select * from scott.emp; BEGIN -- Build subject nbt_body := nbt_body || htf.htmlOpen; nbt_body := nbt_body || htf.headOpen; nbt_body := nbt_body || htf.title( 'List of Employees'); nbt_body := nbt_body || htf.headClose; nbt_body := nbt_body || htf.bodyOpen( cattributes => ' bgcolor="#6666FF" text="#FFFF00"' ); -- Building the Body nbt_body := nbt_body || htf.para; nbt_body := nbt_body || htf.header( 1, 'Current Employee List' || htf.br || htf.img( 'http://www.tusc.com/images/book3.gif', cattributes => ' width="130" height="160"'), calign => 'center'); nbt_body := nbt_body || '<CENTER>'; nbt_body := nbt_body || htf.para; nbt_body := nbt_body || 'Current Employee List'; nbt_body := nbt_body || htf.tableOpen( cattributes => ' width="75%" border="1"' ); nbt_body := nbt_body htf.tableRowOpen; nbt_body := nbt_body htf.tableHeader( 'Name'); nbt_body := nbt_body htf.tableHeader( 'Dept Number'); nbt_body := nbt_body htf.tableHeader( 'Salary'); nbt_body := nbt_body htf.tableRowClose; -- For Loop for emp_rec in emp_cur loop nbt_body := nbt_body htf.tableRowOpen; nbt_body := nbt_body htf.tableData( || || departmental list to each employee or do just about anything I want. Now that is powerful push technology, isn’t it? || || || || || htf.anchor('http://tuscbdb/update_emp_sal ?in_emp_no='|| emp_rec.empno,emp_rec.ename)); nbt_body := nbt_body || htf.tableData( emp_rec.deptno); nbt_body := nbt_body || htf.tableData( emp_rec.sal); nbt_body := nbt_body || htf.tableRowClose; end loop; nbt_body := nbt_body || htf.tableClose; nbt_body := nbt_body || '</CENTER>'; nbt_body := nbt_body || htf.bodyClose; nbt_body := nbt_body || htf.htmlClose; -- Send to All the Employees for emp_rec in emp_cur loop send_mail('smtp.ix.netcom.com', ‘hr@tusc.com', ‘Human Resources', emp_rec.email, emp_rec.ename, 'Y', ‘Current Employee List', 'High', nbt_body); end loop; END; Let’s look at the Email After running this procedure, if I pull up the email in Outlook, I see the following HTML email message: Detailed Reference Let’s break down each of the program units that are part of utl_smtp in greater detail. open_connection open_connection opens a SMTP connection to a SMTP server. When a connection is made successfully, the SMTP host name and port number will be stored in the connection. Parameters host - SMTP host name to connect to port - port number of the SMTP server to connect to c - SMTP connection (OUT) Return value SMTP connection when connection is established, or the SMTP reply (welcome) message - 220 MIT-AI.ARPA Simple Mail Transfer Service Ready Syntax FUNCTION open_connection (host IN VARCHAR2, port IN PLS_INTEGER DEFAULT 25, c OUT connection) RETURN reply; FUNCTION open_connection (host IN VARCHAR2, port IN PLS_INTEGER DEFAULT 25) RETURN connection; Sample code Here is a sample call to open_connection: declare c utl_smtp.connection := utl_smtp.open_connection('smtpserver.tusc.com'); Very impressive, isn’t it? The ability to dynamically build the HTML and dynamically send the email to a group of people is very powerful. I could easily enhance this to send a declare c utl_smtp.connection; begin c := utl_smtp.open_connection(in_smtp_server); Parameters helo Sends HELO command. At the time the transmission channel is opened there is an exchange to ensure that the hosts are communicating with the hosts they think they are communicating with. This command is used to identify the sender-SMTP to the receiver-SMTP. The argument field contains the host name of the sender-SMTP. The receiver-SMTP identifies itself to the sender-SMTP in the connection greeting reply, and in the response to this command. c - SMTP connection domain - domain of the sender Return SMTP reply Syntax FUNCTION ehlo (c IN OUT NOCOPY connection, domain IN VARCHAR2) RETURN replies; Parameters c - SMTP connection domain - domain of the sender Return SMTP reply (for server.tusc.com) example: 250 smtp- PROCEDURE ehlo (c IN OUT NOCOPY connection, domain IN VARCHAR2); Sample Function Call declare replies utl_smtp.replies; replies := utl_smtp.ehlo(c,’tusc.com’); Syntax Sample Procedure Call FUNCTION helo (c IN OUT NOCOPY connection, domain IN VARCHAR2) RETURN reply; utl_smtp.ehlo(c, ‘tusc.com'); PROCEDURE helo (c IN OUT NOCOPY connection, domain IN VARCHAR2); Sample Function Call declare replies utl_smtp.replies; replies := utl_smtp.helo(c,’tusc.com’); mail Sends MAIL command – this is the sender. The first step in the procedure is the MAIL command. The <sender> contains the source mailbox. This command is used to initiate a mail transaction in which the mail data is delivered to one or more mailboxes. The argument field contains a reverse-path. Parameters Sample Procedure Call utl_smtp.helo(c, ‘tusc.com'); ehlo Sends EHLO command. A client SMTP supporting SMTP service extensions should start an SMTP session by issuing the EHLO command instead of the HELO command. If the SMTP server supports the SMTP service extensions, it will give a successful response (250), a failure response (550), or an error response (500, 501, 502, 504, or 421). If the SMTP server does not support any SMTP service extensions, it will generate an error response. c - SMTP connection sender - the sender parameters the optional parameters to MAIL command Return SMTP reply Syntax FUNCTION mail (c IN OUT NOCOPY connection, sender IN VARCHAR2, parameters IN VARCHAR2 DEFAULT NULL) RETURN reply; PROCEDURE mail (c IN OUT NOCOPY connection, sender IN VARCHAR2, parameters IN VARCHAR2 DEFAULT NULL); Sample Procedure Call Sample Function Call open_data declare reply utl_smtp.reply; reply := utl_smtp.mail(c, 'sender@tusc.com'); Sample Procedure Call utl_smtp.mail(c, 'sender@tusc.com'); utl_smtp.rcpt(c, 'recipient@wherever.com'); Sends DATA command. This call opens the data session that the caller makes subsequent write_data() calls to write large piece of data, following by close_data() to close the data session. Basically ends the header section of the SMTP transaction. rcpt Parameters Sends RCPT command – this is the recipient. This command gives a forward-path identifying one recipient. If accepted, the receiver-SMTP returns a 250 OK reply and stores the forwardpath. If the recipient is unknown, the receiverSMTP returns a 550 Failure reply. This second step of the procedure can be repeated any number of times. This command is used to identify an individual recipient of the mail data; multiple recipients are specified by multiple use of this command. c - SMTP connection Parameters c - SMTP connection recipient - the recipient parameters the optional parameters to RCPT command Return SMTP reply Syntax FUNCTION open_data (c IN OUT NOCOPY connection) RETURN reply; PROCEDURE open_data (c IN OUT NOCOPY connection); Sample Function Call declare reply utl_smtp.reply; reply := utl_smtp.open_data(c); Return Sample Procedure Call SMTP reply utl_smtp.open_data(c); Syntax write_data FUNCTION rcpt (c IN OUT NOCOPY connection, recipient IN VARCHAR2, parameters IN VARCHAR2 DEFAULT NULL) RETURN reply; Sends data. This call must be preceded by the call open_data(). This command is used to initiate a mail transaction in which the mail data is delivered to one or more terminals. The argument field contains a reverse-path. This command is successful if the message is delivered to a terminal. PROCEDURE rcpt (c IN OUT NOCOPY connection, recipient IN VARCHAR2, parameters IN VARCHAR2 DEFAULT NULL); Sample Function Call declare reply utl_smtp.reply; reply := utl_smtp.rcpt(c, 'recipient@wherever.com'); Parameters Return None c - SMTP connection data - the data body Syntax quit PROCEDURE write_data(c IN OUT NOCOPY connection, data IN VARCHAR2); Sends QUIT command – transmission channel closing. This command specifies that the receiver must send an OK reply and then close the transmission channel. Sample Procedure Calls utl_smtp.write_data(c, 'Hello, world!'); utl_tcp.CRLF || Parameters if length(in_body) <= 2000 then c - SMTP connection utl_smtp.write_data(nbt_connection, utl_tcp.CRLF || in_body); else utl_smtp.write_data(nbt_connection, utl_tcp.CRLF); for i in 1 .. round(length(in_body)/2000) loop Return utl_smtp.write_data(nbt_connection, substr(in_body, ((i-1)*2000)+1, 2000)); end loop; end if; FUNCTION quit (c IN OUT NOCOPY connection) RETURN reply; close_data PROCEDURE quit (c IN OUT NOCOPY connection); Sends DATA command. This call opens the data session that the caller makes subsequent write_data() calls to write large piece of data, following by close_data() to close the data session. Parameters c - SMTP connection Return SMTP reply Syntax FUNCTION close_data (c IN OUT NOCOPY connection) RETURN reply; PROCEDURE close_data (c IN OUT NOCOPY connection); SMTP reply (221 smtp-server.tusc.com Service closing transmission channel) Syntax Sample Function Call declare reply utl_smtp.reply; reply := utl_smtp.quit(c); Sample Procedure Call utl_smtp.quit(c); rset Sends RSET command. This command specifies that the current mail transaction is to be aborted. Any stored sender, recipients, and mail data must be discarded, and all buffers and state tables cleared. The receiver must send an OK reply. Parameters Return Sample Function Call SMTP reply declare reply utl_smtp.reply; reply := utl_smtp.close_data(c); Syntax Sample Procedure Call utl_smtp.close_data(c); c - SMTP connection FUNCTION rset (c IN OUT NOCOPY connection) RETURN reply; PROCEDURE rset (c IN OUT NOCOPY connection); Sample Function Call help declare reply utl_smtp.reply; reply := utl_smtp.rset(c); Sends HELP command. This command causes the receiver to send helpful information to the sender of the HELP command. The command may take an argument (e.g., any command name) and return more specific information as a response. Sample Procedure Call utl_smtp.rset(c); vrfy Sends VRFY command, which allows you to perform a fuzzy search for the username. This command asks the receiver to confirm that the argument identifies a user. If it is a user name, the full name of the user (if known) and the fully specified mailbox are returned. Note – Microsoft Exchange delivers the message “252 Cannot verify user” when vrfy is turned off. In other words, it returns a success code, but doesn’t do anything. Parameters c - SMTP connection recipient - the recipient to verify Parameters c - SMTP connection command - the command to get help message Return SMTP reply Syntax FUNCTION help (c IN OUT NOCOPY connection, command IN VARCHAR2 DEFAULT NULL) RETURN replies; Sample Function Call Return declare replies utl_smtp.replies; replies := utl_smtp.help(nbt_connection, 'VRFY'); SMTP reply Syntax Sample Output 214 Commands: 214 HELO MAIL RCPT DATA RSET 214 NOOP QUIT HELP VRFY ETRN 214 XEXCH50 STARTTLS AUTH 214 End of HELP info FUNCTION vrfy (c IN OUT NOCOPY connection, recipient IN VARCHAR2) RETURN reply; Fuzzy Search w/ vrfy noop reply := utl_smtp.vrfy(c, ‘Smith’); Sample reply: <Smith@tusc.com> 250 Fred Smith reply := utl_smtp.vrfy(c, ‘Smithy’); Sample reply: 251 User not local; will forward to <Smithy@denver.tusc.com> reply := utl_smtp.vrfy(c, ‘Jones’); Sample reply: 550 String does not match anything. reply := utl_smtp.vrfy(c, ‘Jonesy’); Sends NOOP command. This command does not affect any parameters or previously entered commands. It specifies no action other than the receiver should send an OK reply. Parameters c - SMTP connection Return SMTP reply Sample reply: 551 User not local; please try <Jones@denver.tusc.com> Syntax reply := ‘Gourzenkyinplatz’); FUNCTION noop (c IN OUT NOCOPY connection) RETURN reply; utl_smtp.vrfy(c, Sample reply: 553 User ambiguous. Parameters PROCEDURE noop (c IN OUT NOCOPY connection); Sample Function Call declare reply utl_smtp.reply; reply := utl_smtp.noop(c); Return SMTP reply Sample Procedure Call utl_smtp.noop(c); Syntax data Sends DATA command. The data will be closed by the sequence <CR><LF>.<CR><LF>. If accepted, the receiver-SMTP returns a 354 Intermediate reply and considers all succeeding lines to be the message text. When the end of text is received and stored the SMTP-receiver sends a 250 OK reply. The receiver treats the lines following the command as mail data from the sender. This command causes the mail data from this command to be appended to the mail data buffer. The mail data may contain any of the 128 ASCII character codes. Parameters c - SMTP connection body - the data body FUNCTION command (c IN OUT NOCOPY connection, cmd IN VARCHAR2, arg IN VARCHAR2 DEFAULT NULL) RETURN reply; PROCEDURE command (c IN OUT NOCOPY connection, cmd IN VARCHAR2, arg IN VARCHAR2 DEFAULT NULL); command_replies Sends a generic SMTP command and retrieves multiple reply lines. Could be used for VRFY, EXPN, TURN. Parameters Return SMTP reply after the <CR><LF><CR><LF> is sent c - SMTP connection cmd – SMTP command arg - optional argument to the SMTP command sequence Syntax FUNCTION data (c IN OUT NOCOPY connection, body IN VARCHAR2) RETURN reply; c - SMTP connection cmd - SMTP command arg - optional argument to the SMTP command Return SMTP reply Syntax PROCEDURE data (c IN OUT NOCOPY connection, body IN VARCHAR2); FUNCTION command_replies (c IN OUT NOCOPY connection, cmd IN VARCHAR2, arg IN VARCHAR2 DEFAULT NULL) RETURN replies; command EXPN – Expand List Sends a generic SMTP command and retrieves a single reply line. If multiple reply lines are returned from the SMTP server, the last reply line is returned. Could be used for VRFY and TURN. This command asks the receiver to confirm that the argument identifies a mailing list, and if so, to return the membership of that list. The full name of the users (if known) and the fully specified mailboxes are returned in a multiline reply. Command Replies Example Biography declare replies utl_smtp.replies; replies := utl_smtp.command_replies (c, ‘EXPN’, ‘Management’) Bradley D. Brown is Chairman and Chief Architect of TUSC (The Ultimate Software Consultants), a 1998 Inc. 500 full-service consulting company specializing in Oracle with offices in Chicago, Denver, Detroit and Milwaukee. In June of 1998, Brown authored the title Oracle Application Server Web Toolkit Reference that is part of the Oracle Press Tips & Techniques series TUSC is writing for Osborne/McGraw-Hill. His second book, Oracle8i Web Development, came out in December 1999. Brad has been working with management information systems for more than 18 years, including the last 13 with a focus on Oracle. Sample reply (replies(i).code || replies(i).text): 250-Bradley D. Brown <bradley.brown@denver.tusc.com> 250-Richard J. Niemiec <richard.niemiec@chicago.tusc.com> 250-Joseph C. Trezzo <joseph.trezzo@chicago.tusc.com> Note – the mail server administrator can choose not to implement this feature. If the feature is not implemented, the following error will be returned: 502-command not implemented References The following references are helpful for SMTP: SMTP RFC 821 Specification http://info.broker.isi.edu/innotes/rfc/files/rfc821.txt http://www.landfield.com/rfcs/rfc821.html SMTP RFC 1869 Specification (SMTP Extensions) http://info.broker.isi.edu/innotes/rfc/files/rfc1869.txt http://www.xyweb.com/rfc/rfc1869.html SMTP Header http://www.arsdigita.com/asj/mime/ http://www.faqs.org/rfcs/rfc822.html http://www.faqs.org/rfcs/rfc1049.html Summary I’m confident that after reading all about utl_smtp, you can see that this is a powerful utility and capability. The ability to dynamically build emails and the ability to dynamically send to users in the database is power! You can use this to email passwords to users, send reports, and push any information you wish to distribute. So are you excited yet?