diff --git a/packages/app_actions.spec.sql b/packages/app_actions.spec.sql index 50caac5..d9a8e27 100644 --- a/packages/app_actions.spec.sql +++ b/packages/app_actions.spec.sql @@ -32,6 +32,14 @@ CREATE OR REPLACE PACKAGE app_actions AS settings_package CONSTANT VARCHAR2(30) := 'SETT'; settings_prefix CONSTANT VARCHAR2(30) := 'get_'; + -- for sending emails + smtp_from CONSTANT VARCHAR2(200) := ''; + smtp_username CONSTANT VARCHAR2(50) := NULL; + smtp_password CONSTANT VARCHAR2(50) := NULL; + smtp_host CONSTANT VARCHAR2(50) := ''; + smtp_port CONSTANT NUMBER(4) := 25; + smtp_timeout CONSTANT NUMBER(2) := 20; + @@ -235,5 +243,23 @@ CREATE OR REPLACE PACKAGE app_actions AS -- PROCEDURE refresh_user_source_views; + + + -- + -- Send UTF_8 email with compressed attachements + -- + PROCEDURE send_mail ( + in_to VARCHAR2, + in_subject VARCHAR2, + in_body CLOB, + in_cc VARCHAR2 := NULL, + in_bcc VARCHAR2 := NULL, + in_from VARCHAR2 := NULL, + in_attach_name VARCHAR2 := NULL, + in_attach_mime VARCHAR2 := NULL, + in_attach_data CLOB := NULL, + in_compress BOOLEAN := FALSE + ); + END; / diff --git a/packages/app_actions.sql b/packages/app_actions.sql index ff5bea2..ae08023 100644 --- a/packages/app_actions.sql +++ b/packages/app_actions.sql @@ -816,5 +816,195 @@ CREATE OR REPLACE PACKAGE BODY app_actions AS app.raise_error(); END; + + + PROCEDURE send_mail ( + in_to VARCHAR2, + in_subject VARCHAR2, + in_body CLOB, + in_cc VARCHAR2 := NULL, + in_bcc VARCHAR2 := NULL, + in_from VARCHAR2 := NULL, + in_attach_name VARCHAR2 := NULL, + in_attach_mime VARCHAR2 := NULL, + in_attach_data CLOB := NULL, + in_compress BOOLEAN := FALSE + ) AS + boundary CONSTANT VARCHAR2(80) := '-----5b9d8059445a8eb8c025f159131f02d94969a12c16363d4dec42e893b374cb85-----'; + -- + reply UTL_SMTP.REPLY; + conn UTL_SMTP.CONNECTION; + -- + blob_content BLOB; + blob_gzipped BLOB; + blob_amount BINARY_INTEGER := 6000; + blob_offset PLS_INTEGER := 1; + buffer VARCHAR2(24000); + buffer_raw RAW(6000); + -- + FUNCTION quote_encoding ( + in_text VARCHAR2 + ) + RETURN VARCHAR2 AS + BEGIN + RETURN '=?UTF-8?Q?' || REPLACE( + UTL_RAW.CAST_TO_VARCHAR2(UTL_ENCODE.QUOTED_PRINTABLE_ENCODE( + UTL_RAW.CAST_TO_RAW(in_text))), '=' || UTL_TCP.CRLF, '') || '?='; + END; + -- + FUNCTION quote_address ( + in_address VARCHAR2, + in_strip_name BOOLEAN := FALSE + ) + RETURN VARCHAR2 AS + in_found PLS_INTEGER; + BEGIN + IF in_strip_name THEN + RETURN REGEXP_REPLACE(in_address, '.*\s?<(\S+)>$', '\1'); + ELSE + in_found := REGEXP_INSTR(in_address, '\s?<\S+@\S+\.\S{2,6}>$'); + IF in_found > 1 THEN + RETURN quote_encoding(RTRIM(SUBSTR(in_address, 1, in_found))) || SUBSTR(in_address, in_found); + ELSE + RETURN in_address; + END IF; + END IF; + END; + -- + FUNCTION clob_to_blob ( + in_clob CLOB + ) + RETURN BLOB AS + out_blob BLOB; + -- + v_file_size INTEGER := DBMS_LOB.LOBMAXSIZE; + v_dest_offset INTEGER := 1; + v_src_offset INTEGER := 1; + v_blob_csid NUMBER := DBMS_LOB.DEFAULT_CSID; + v_lang_context NUMBER := DBMS_LOB.DEFAULT_LANG_CTX; + v_warning INTEGER; + v_length NUMBER; + BEGIN + DBMS_LOB.CREATETEMPORARY(out_blob, TRUE); + DBMS_LOB.CONVERTTOBLOB(out_blob, in_clob, v_file_size, v_dest_offset, v_src_offset, v_blob_csid, v_lang_context, v_warning); + RETURN out_blob; + END; + -- + PROCEDURE split_addresses ( + in_out_conn IN OUT NOCOPY UTL_SMTP.CONNECTION, + in_to IN VARCHAR2 + ) + AS + BEGIN + FOR i IN ( + SELECT LTRIM(RTRIM(REGEXP_SUBSTR(in_to, '[^;,]+', 1, LEVEL))) AS address + FROM DUAL + CONNECT BY REGEXP_SUBSTR(in_to, '[^;,]+', 1, LEVEL) IS NOT NULL) + LOOP + UTL_SMTP.RCPT(in_out_conn, quote_address(i.address, TRUE)); + END LOOP; + END; + BEGIN + app.log_module(in_to, in_subject, in_cc, in_bcc, in_attach_name); + + -- connect to SMTP server + reply := UTL_SMTP.OPEN_CONNECTION(smtp_host, smtp_port, conn, smtp_timeout); + UTL_SMTP.HELO(conn, smtp_host); + IF smtp_username IS NOT NULL THEN + UTL_SMTP.COMMAND(conn, 'AUTH LOGIN'); + UTL_SMTP.COMMAND(conn, UTL_ENCODE.BASE64_ENCODE(UTL_RAW.CAST_TO_RAW(smtp_username))); + IF smtp_password IS NOT NULL THEN + UTL_SMTP.COMMAND(conn, UTL_ENCODE.BASE64_ENCODE(UTL_RAW.CAST_TO_RAW(smtp_password))); + END IF; + END IF; + + -- prepare headers + UTL_SMTP.MAIL(conn, quote_address(COALESCE(in_from, smtp_from), TRUE)); + + -- handle multiple recipients + split_addresses(conn, in_to); + -- + IF in_cc IS NOT NULL THEN + split_addresses(conn, in_cc); + END IF; + -- + IF in_bcc IS NOT NULL THEN + split_addresses(conn, in_bcc); + END IF; + + -- continue with headers + UTL_SMTP.OPEN_DATA(conn); + -- + UTL_SMTP.WRITE_DATA(conn, 'Date: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS') || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'From: ' || quote_address(COALESCE(in_from, smtp_from)) || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'To: ' || quote_address(in_to) || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Subject: ' || quote_encoding(in_subject) || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Reply-To: ' || in_from || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'MIME-Version: 1.0' || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Type: multipart/mixed; boundary="' || boundary || '"' || UTL_TCP.CRLF || UTL_TCP.CRLF); + + -- prepare body content + IF in_body IS NOT NULL THEN + UTL_SMTP.WRITE_DATA(conn, '--' || boundary || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Type: ' || 'text/html' || '; charset="utf-8"' || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Transfer-Encoding: base64' || UTL_TCP.CRLF || UTL_TCP.CRLF); + -- + FOR i IN 0 .. TRUNC((DBMS_LOB.GETLENGTH(in_body) - 1) / 12000) LOOP + UTL_SMTP.WRITE_RAW_DATA(conn, UTL_ENCODE.BASE64_ENCODE(UTL_RAW.CAST_TO_RAW(DBMS_LOB.SUBSTR(in_body, 12000, i * 12000 + 1)))); + END LOOP; + -- + UTL_SMTP.WRITE_DATA(conn, UTL_TCP.CRLF || UTL_TCP.CRLF); + END IF; + + -- prepare attachment + IF in_attach_name IS NOT NULL AND in_compress THEN + -- compress attachment + UTL_SMTP.WRITE_DATA(conn, '--' || boundary || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Transfer-Encoding: base64' || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Type: ' || 'application/octet-stream' || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Disposition: attachment; filename="' || in_attach_name || '.gz"' || UTL_TCP.CRLF || UTL_TCP.CRLF); + -- + blob_content := clob_to_blob(in_attach_data); + DBMS_LOB.CREATETEMPORARY(blob_gzipped, TRUE, DBMS_LOB.CALL); + DBMS_LOB.OPEN(blob_gzipped, DBMS_LOB.LOB_READWRITE); + -- + UTL_COMPRESS.LZ_COMPRESS(blob_content, blob_gzipped, quality => 8); + -- + WHILE blob_offset <= DBMS_LOB.GETLENGTH(blob_gzipped) LOOP + DBMS_LOB.READ(blob_gzipped, blob_amount, blob_offset, buffer_raw); + UTL_SMTP.WRITE_RAW_DATA(conn, UTL_ENCODE.BASE64_ENCODE(buffer_raw)); + blob_offset := blob_offset + blob_amount; + END LOOP; + DBMS_LOB.FREETEMPORARY(blob_gzipped); + -- + UTL_SMTP.WRITE_DATA(conn, UTL_TCP.CRLF || UTL_TCP.CRLF); + ELSIF in_attach_name IS NOT NULL THEN + -- regular attachment + UTL_SMTP.WRITE_DATA(conn, '--' || boundary || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Transfer-Encoding: base64' || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Type: ' || in_attach_mime || '; name="' || in_attach_name || '"' || UTL_TCP.CRLF); + UTL_SMTP.WRITE_DATA(conn, 'Content-Disposition: attachment; filename="' || in_attach_name || '"' || UTL_TCP.CRLF || UTL_TCP.CRLF); + -- + FOR i IN 0 .. TRUNC((DBMS_LOB.GETLENGTH(in_attach_data) - 1) / 12000) LOOP + UTL_SMTP.WRITE_RAW_DATA(conn, UTL_ENCODE.BASE64_ENCODE(UTL_RAW.CAST_TO_RAW(DBMS_LOB.SUBSTR(in_attach_data, 12000, i * 12000 + 1)))); + END LOOP; + -- + UTL_SMTP.WRITE_DATA(conn, UTL_TCP.CRLF || UTL_TCP.CRLF); + END IF; + + -- close + UTL_SMTP.WRITE_DATA(conn, '--' || boundary || '--' || UTL_TCP.CRLF); + UTL_SMTP.CLOSE_DATA(conn); + UTL_SMTP.QUIT(conn); + EXCEPTION + WHEN UTL_SMTP.TRANSIENT_ERROR OR UTL_SMTP.PERMANENT_ERROR THEN + BEGIN + UTL_SMTP.QUIT(conn); + EXCEPTION + WHEN UTL_SMTP.TRANSIENT_ERROR OR UTL_SMTP.PERMANENT_ERROR THEN + NULL; + END; + END; + END; /