本文為完整郵件發送套件的**第四部分**,涵蓋:**字串分割處理 (收件人/附件路徑)**、**郵件頭與正文寫入**、 **MIME 邊界結尾**、 **附件總控程序 ATTACHMENT**、**文字附件逐行讀取傳輸**、 **二進制附件處理入口**。
PROCEDURE P_SPLITE_STR (
P_STR VARCHAR2,
P_SPLITE_FLAG INT DEFAULT 1 -- 1: 收件人地址, 2: 附件檔名
) IS
L_ADDR VARCHAR2(254) := '';
L_LEN INT;
L_STR VARCHAR2(4000);
J INT := 0;
BEGIN
-- 正規化:去空格、分號轉逗號、去首尾逗號
L_STR := TRIM(RTRIM(REPLACE(REPLACE(P_STR, ';', ','), ' ', ''), ','));
L_LEN := LENGTH(L_STR);
FOR I IN 1 .. L_LEN LOOP
IF SUBSTR(L_STR, I, 1) <> ',' THEN
L_ADDR := L_ADDR || SUBSTR(L_STR, I, 1);
ELSE
J := J + 1;
IF P_SPLITE_FLAG = 1 THEN
-- 收件人格式:
L_ADDR := '<' || L_ADDR || '>';
MY_ADDRESS_LIST(J) := L_ADDR;
ELSIF P_SPLITE_FLAG = 2 THEN
-- 附件路徑
MY_ACCT_LIST(J) := L_ADDR;
END IF;
L_ADDR := '';
END IF;
-- 處理最後一個元素 (無尾隨逗號)
IF I = L_LEN THEN
J := J + 1;
IF P_SPLITE_FLAG = 1 THEN
L_ADDR := '<' || L_ADDR || '>';
MY_ADDRESS_LIST(J) := L_ADDR;
ELSIF P_SPLITE_FLAG = 2 THEN
MY_ACCT_LIST(J) := L_ADDR;
END IF;
END IF;
END LOOP;
END P_SPLITE_STR;
PROCEDURE WRITE_DATA (
P_CONN IN OUT NOCOPY UTL_SMTP.CONNECTION,
P_NAME IN VARCHAR2,
P_VALUE IN VARCHAR2,
P_SPLITE VARCHAR2 DEFAULT ':',
P_CRLF VARCHAR2 DEFAULT L_CRLF
) IS
BEGIN
-- 關鍵:UTL_RAW.CAST_TO_RAW + CONVERT 解決中文亂碼
UTL_SMTP.WRITE_RAW_DATA(
P_CONN,
UTL_RAW.CAST_TO_RAW(
CONVERT(P_NAME || P_SPLITE || P_VALUE || P_CRLF, 'ZHS16GBK')
)
);
END WRITE_DATA;
PROCEDURE END_BOUNDARY (
CONN IN OUT NOCOPY UTL_SMTP.CONNECTION,
LAST IN BOOLEAN DEFAULT FALSE
) IS
BEGIN
UTL_SMTP.WRITE_DATA(CONN, UTL_TCP.CRLF); -- 空行
IF LAST THEN
UTL_SMTP.WRITE_DATA(CONN, LAST_BOUNDARY); -- --BOUNDARY--
END IF;
END END_BOUNDARY;
PROCEDURE ATTACHMENT (
CONN IN OUT NOCOPY UTL_SMTP.CONNECTION,
MIME_TYPE IN VARCHAR2 DEFAULT 'text/plain',
INLINE IN BOOLEAN DEFAULT TRUE,
FILENAME IN VARCHAR2 DEFAULT 't.txt',
TRANSFER_ENC IN VARCHAR2 DEFAULT 'bit 7', -- 'bit 7'=文字, 'base64'=二進制
DT_NAME IN VARCHAR2 DEFAULT '0' -- 動態目錄物件名稱前綴
) IS
L_FILENAME VARCHAR2(1000);
BEGIN
-- 寫入 MIME 邊界開頭
UTL_SMTP.WRITE_DATA(CONN, FIRST_BOUNDARY);
-- 附件 Content-Type
WRITE_DATA(CONN, 'Content-Type', MIME_TYPE);
-- 刪除舊同名目錄物件 (避免衝突)
DROP_DIRECTORY(DT_NAME);
-- 建立動態目錄物件指向附件所在目錄
CREATE_DIRECTORY(DT_NAME, GET_FILE(FILENAME, 1));
-- 解析純檔名
L_FILENAME := GET_FILE(FILENAME, 2);
-- Content-Disposition: inline (內嵌顯示) 或 attachment (下載)
IF INLINE THEN
WRITE_DATA(CONN, 'Content-Disposition', 'inline; filename="' || L_FILENAME || '"');
ELSE
WRITE_DATA(CONN, 'Content-Disposition', 'attachment; filename="' || L_FILENAME || '"');
END IF;
-- Transfer-Encoding
IF TRANSFER_ENC IS NOT NULL THEN
WRITE_DATA(CONN, 'Content-Transfer-Encoding', TRANSFER_ENC);
END IF
UTL_SMTP.WRITE_DATA(CONN, UTL_TCP.CRLF); -- 空行分隔頭與體
-- === 附件內容傳輸分支 ===
IF TRANSFER_ENC = 'bit 7' THEN
-- 文字附件:UTL_FILE 逐行讀取
BEGIN
L_FILE_HANDLE := UTL_FILE.FOPEN(DT_NAME, L_FILENAME, 'r');
LOOP
UTL_FILE.GET_LINE(L_FILE_HANDLE, L_LINE);
L_MESG := L_LINE || L_CRLF;
WRITE_DATA(CONN, '', L_MESG, '', '');
END LOOP;
UTL_FILE.FCLOSE(L_FILE_HANDLE);
END_BOUNDARY(CONN);
EXCEPTION
WHEN OTHERS THEN
UTL_FILE.FCLOSE(L_FILE_HANDLE);
END_BOUNDARY(CONN);
NULL; -- 吞沒例外 (建議改為 RAISE)
END;
ELSIF TRANSFER_ENC = 'base64' THEN
-- 二進制附件:轉入 DBMS_LOB 分片處理 (見第五部分)
BEGIN
-- 此處呼叫第五部分的分片邏輯
NULL; -- 佔位
END;
END IF;
END ATTACHMENT;
FUNCTION GET_FILE (P_FILE VARCHAR2, P_GET INT) RETURN VARCHAR2 IS
-- P_GET=1: 返回目錄路徑, P_GET=2: 返回檔名
L_FILE VARCHAR2(1000);
BEGIN
IF INSTR(P_FILE, '\') > 0 THEN
-- Windows 路徑
IF P_GET = 1 THEN
L_FILE := SUBSTR(P_FILE, 1, INSTR(P_FILE, '\', -1) - 1);
ELSIF P_GET = 2 THEN
L_FILE := SUBSTR(P_FILE, -(LENGTH(P_FILE) - INSTR(P_FILE, '\', -1)));
END IF;
ELSIF INSTR(P_FILE, '/') > 0 THEN
-- Linux/Unix 路徑
IF P_GET = 1 THEN
L_FILE := SUBSTR(P_FILE, 1, INSTR(P_FILE, '/', -1) - 1);
ELSIF P_GET = 2 THEN
L_FILE := SUBSTR(P_FILE, -(LENGTH(P_FILE) - INSTR(P_FILE, '/', -1)));
END IF;
END IF;
RETURN L_FILE;
END GET_FILE;
PROCEDURE DROP_DIRECTORY (P_DIRECTORY_NAME VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE 'DROP DIRECTORY ' || P_DIRECTORY_NAME;
EXCEPTION
WHEN OTHERS THEN NULL; -- 不存在時忽略
END DROP_DIRECTORY;
PROCEDURE CREATE_DIRECTORY (P_DIRECTORY_NAME VARCHAR2, P_DIR VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE 'CREATE DIRECTORY ' || P_DIRECTORY_NAME || ' AS ''' || P_DIR || '''';
EXECUTE IMMEDIATE 'GRANT READ, WRITE ON DIRECTORY ' || P_DIRECTORY_NAME || ' TO PUBLIC';
EXCEPTION
WHEN OTHERS THEN RAISE;
END CREATE_DIRECTORY;
PROCSENDEMAIL (主入口)
├─ P_SPLITE_STR (字串分割)
├─ GET_FILE (路徑解析)
├─ CREATE_DIRECTORY / DROP_DIRECTORY (動態目錄)
├─ WRITE_DATA (頭/體寫入 + 編碼轉換)
├─ END_BOUNDARY (MIME 邊界)
├─ ATTACHMENT (附件總控)
│ ├─ 文字分支: UTL_FILE.GET_LINE 逐行
│ └─ 二進制分支: DBMS_LOB 分片 Base64 (第五部分)
└─ P_EMAIL (SMTP 會話總控)
├─ OPEN_CONNECTION / HELO / AUTH
├─ MAIL / RCPT / OPEN_DATA
├─ 寫入頭、正文、附件
└─ CLOSE_DATA / QUIT
| 決策點 | 選擇 | 理由 |
|---|---|---|
| 動態目錄物件 | 每個附件建立獨立目錄物件 (DIR_FOR_SEND_MAIL1, 2...) | 避免並發衝突、權限隔離、用完即刪 |
| 編碼轉換 | CONVERT + UTL_RAW.CAST_TO_RAW + WRITE_RAW_DATA | 徹底解決中文亂碼,支援任意資料庫字符集 |
| 文字附件傳輸 | UTL_FILE.GET_LINE 逐行 | 記憶體佔用極低,支援超大文字檔 |
| 二進制附件傳輸 | DBMS_LOB 分片 + Base64 (2016 位元組/片) | 記憶體恆定、支援 GB 級附件、符合 SMTP 行長度 |
| 收件人格式 | 自動包裝 <email@domain> | 符合 RFC 5321,避免郵箱拒收 |
| 多行正文傳遞 | Shell 以 ++ 分隔,PL/SQL 還原 CHR(10) | 繞過命令列換行限制 |
此為生產級郵件發送套件核心邏輯 (第 4/5 部分),配合第一~三部分 (參數宣告、主程序入口、Shell 調用) 可構建完整可用系統。部署時必須配置 ACL 授權 SMTP 主機埠。