本文為完整郵件發送套件的**第五部分**,專注於**二進制附件分片編碼傳輸核心邏輯**與**主發送流程整合**。前四部分涵蓋:參數定義、字串分割、郵件頭寫入、MIME 邊界處理、文字附件處理、目錄物件管理、檔案路徑解析。
-- 變數說明:
-- L_FIL: BFILE 定位器
-- L_FILEPOS: 讀取位移
-- L_AMT: 每次讀取量 (2016 bytes = 672*3,Base64 編碼後恰好 2688 字元,符合 SMTP 行長度限制)
-- L_BUF: RAW 暫存區
-- L_DATA: 累積編碼資料
-- L_PIECES: 總分片數
-- L_MAX_LINE_WIDTH: 54 (Base64 編碼前每行位元組數)
-- L_CHUNKS: 編碼後行數
L_FILEPOS := 1; -- 重置位移 (多附件時必要)
L_FIL := BFILENAME(DT_NAME, L_FILENAME);
L_FILE_LEN := DBMS_LOB.GETLENGTH(L_FIL);
L_MODULO := MOD(L_FILE_LEN, L_AMT);
L_PIECES := TRUNC(L_FILE_LEN / L_AMT);
IF L_MODULO <> 0 THEN L_PIECES := L_PIECES + 1; END IF;
DBMS_LOB.FILEOPEN(L_FIL, DBMS_LOB.FILE_READONLY);
DBMS_LOB.READ(L_FIL, L_AMT, L_FILEPOS, L_BUF);
L_DATA := NULL;
FOR I IN 1 .. L_PIECES LOOP
L_FILEPOS := I * L_AMT + 1;
L_FILE_LEN := L_FILE_LEN - L_AMT;
-- 累積 Base64 編碼
L_DATA := UTL_RAW.CONCAT(L_DATA, L_BUF);
-- 計算編碼後行數
L_CHUNKS := TRUNC(UTL_RAW.LENGTH(L_DATA) / L_MAX_LINE_WIDTH);
IF I <> L_PIECES THEN L_CHUNKS := L_CHUNKS - 1; END IF;
-- 寫入 SMTP (自動換行)
UTL_SMTP.WRITE_RAW_DATA(CONN, UTL_ENCODE.BASE64_ENCODE(L_DATA));
L_DATA := NULL;
-- 最後一片可能小於 L_AMT
IF L_FILE_LEN < L_AMT AND L_FILE_LEN > 0 THEN
L_AMT := L_FILE_LEN;
END IF;
-- 讀取下一片
DBMS_LOB.READ(L_FIL, L_AMT, L_FILEPOS, L_BUF);
END LOOP;
DBMS_LOB.FILECLOSE(L_FIL);
END_BOUNDARY(CONN);
| 設計點 | 技術細節 | 目的 |
|---|---|---|
| L_AMT = 672 * 3 = 2016 | Base64 編碼:3 位元組 → 4 字元。2016 位元組編碼後 = 2688 字元 | 符合 SMTP 單行長度限制 (通常 1000 字元),並能被 54 整除便於分行 |
| L_MAX_LINE_WIDTH = 54 | Base64 編碼前每行 54 位元組 → 編碼後 72 字元 | 符合 MIME 建議行長度 (76 字元內),避免郵件伺服器截斷 |
| 分片迴圈累積 L_DATA | 每片編碼後立即寫入,不在記憶體累積完整檔案 | 支援**超大附件 (GB 級)**,避免 PGA/OOM |
| UTL_RAW.CONCAT + UTL_ENCODE.BASE64_ENCODE | RAW 型別操作,避免字符集轉換干擾二進制資料 | 保證二進制附件 (圖片、PDF、ZIP) 完整性 |
| 例外處理區塊 | 任何錯誤先關閉 BFILE,寫入 MIME 結尾邊界,再拋出 | 確保郵件結構完整,收件端能正常解析 |
PROCEDURE P_EMAIL (
P_SENDORADDRESS2 VARCHAR2, -- 發件人
P_RECEIVERADDRESS2 VARCHAR2 -- 收件人
) IS
L_CONN UTL_SMTP.CONNECTION;
BEGIN
-- 1. 建立 SMTP 連線
L_CONN := UTL_SMTP.OPEN_CONNECTION(P_SERVER, P_PORT);
UTL_SMTP.HELO(L_CONN, P_SERVER);
-- 2. SMTP 認證 (可選)
IF P_NEED_SMTP = 1 THEN
UTL_SMTP.COMMAND(L_CONN, 'AUTH LOGIN', '');
UTL_SMTP.COMMAND(L_CONN, UTL_RAW.CAST_TO_VARCHAR2(UTL_ENCODE.BASE64_ENCODE(UTL_RAW.CAST_TO_RAW(P_USER))));
UTL_SMTP.COMMAND(L_CONN, UTL_RAW.CAST_TO_VARCHAR2(UTL_ENCODE.BASE64_ENCODE(UTL_RAW.CAST_TO_RAW(P_PASS))));
END IF;
-- 3. 發件人/收件人
UTL_SMTP.MAIL(L_CONN, P_SENDORADDRESS2);
UTL_SMTP.RCPT(L_CONN, P_RECEIVERADDRESS2);
-- 4. 郵件資料階段
UTL_SMTP.OPEN_DATA(L_CONN);
-- 郵件頭
WRITE_DATA(L_CONN, 'Date', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
WRITE_DATA(L_CONN, 'From', P_SENDOR);
WRITE_DATA(L_CONN, 'To', P_RECEIVER);
WRITE_DATA(L_CONN, 'Subject', P_SUB);
WRITE_DATA(L_CONN, 'Content-Type', MULTIPART_MIME_TYPE); -- multipart/mixed; boundary="-----BYSUK"
UTL_SMTP.WRITE_DATA(L_CONN, UTL_TCP.CRLF);
UTL_SMTP.WRITE_DATA(L_CONN, FIRST_BOUNDARY); -- --BOUNDARY
-- 純文字正文 (gb2312 編碼)
WRITE_DATA(L_CONN, 'Content-Type', 'text/plain;charset=gb2312');
UTL_SMTP.WRITE_DATA(L_CONN, UTL_TCP.CRLF); -- 空行分隔頭與體
-- 正文內容處理:將 Shell 傳入的分隔符還原為換行
WRITE_DATA(L_CONN, '',
REPLACE(REPLACE(P_TXT, L_SPLITE, CHR(10)), CHR(10), L_CRLF),
'', '');
END_BOUNDARY(L_CONN);
-- 5. 附件處理 (若有)
IF P_FILENAME IS NOT NULL THEN
P_SPLITE_STR(P_FILENAME, 2); -- 解析附件路徑列表 (分號/逗號分隔)
FOR K IN 1 .. MY_ACCT_LIST.COUNT LOOP
ATTACHMENT(
CONN => L_CONN,
FILENAME => MY_ACCT_LIST(K),
TRANSFER_ENC => P_ENCODE, -- 'bit 7' (文字) 或 'base64' (二進制)
DT_NAME => L_DIRECTORY_BASE_NAME || TO_CHAR(K) -- 動態目錄名 DIR_FOR_SEND_MAIL1, 2...
);
END LOOP;
END IF;
-- 6. 結束
UTL_SMTP.CLOSE_DATA(L_CONN);
UTL_SMTP.QUIT(L_CONN);
EXCEPTION
WHEN OTHERS THEN
NULL; -- 原代碼吞沒例外,建議改為 RAISE 或記錄日誌
RAISE;
END P_EMAIL;
PROCSENDEMAIL (主入口)
├─ P_SPLITE_STR (字串分割: 收件人/附件路徑)
├─ GET_FILE (路徑解析: 目錄/檔名)
├─ CREATE_DIRECTORY / DROP_DIRECTORY (動態目錄物件管理)
├─ WRITE_DATA (郵件頭/正文寫入: 字符集轉換 ZHS16GBK)
├─ 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
L_CRLF VARCHAR2(2) := UTL_TCP.CRLF; -- CHR(13)||CHR(10)
L_SPLITE VARCHAR2(10) := '++'; -- Shell 多行傳遞分隔符
BOUNDARY CONSTANT VARCHAR2(256) := '-----BYSUK';
FIRST_BOUNDARY CONSTANT VARCHAR2(256) := '--' || BOUNDARY || L_CRLF;
LAST_BOUNDARY CONSTANT VARCHAR2(256) := '--' || BOUNDARY || '--' || L_CRLF;
MULTIPART_MIME_TYPE CONSTANT VARCHAR2(256) := 'multipart/mixed; boundary="' || BOUNDARY || '"';
-- 二進制附件變數
L_FIL BFILE;
L_FILE_LEN NUMBER;
L_MODULO NUMBER;
L_PIECES NUMBER;
L_FILE_HANDLE UTL_FILE.FILE_TYPE;
L_AMT BINARY_INTEGER := 672 * 3; -- 2016
L_FILEPOS PLS_INTEGER := 1;
L_CHUNKS NUMBER;
L_BUF RAW(2100);
L_DATA RAW(2100);
L_MAX_LINE_WIDTH NUMBER := 54;
L_DIRECTORY_BASE_NAME VARCHAR2(100) := 'DIR_FOR_SEND_MAIL';
L_LINE VARCHAR2(1000);
L_MESG VARCHAR2(32767);
-- 集合型別
TYPE ADDRESS_LIST IS TABLE OF VARCHAR2(100) INDEX BY BINARY_INTEGER;
MY_ADDRESS_LIST ADDRESS_LIST;
TYPE ACCT_LIST IS TABLE OF VARCHAR2(100) INDEX BY BINARY_INTEGER;
MY_ACCT_LIST ACCT_LIST;
BEGIN
PROCSENDEMAIL(
P_TXT => '第一行正文++第二行正文++第三行正文', -- 用 ++ 分隔多行
P_SUB => '測試郵件主旨',
P_SENDOR => 'sender@company.com',
P_RECEIVER => 'receiver1@company.com;receiver2@company.com', -- 多收件人分號分隔
P_SERVER => 'smtp.company.com',
P_PORT => 25,
P_NEED_SMTP => 1,
P_USER => 'smtp_user',
P_PASS => 'smtp_pass',
P_FILENAME => '/data/reports/report.pdf;/data/attach/image.png', -- 多附件分號分隔
P_ENCODE => 'base64' -- 二進制附件用 base64
);
END;
/
| 坑點 | 後果 | 解決 |
|---|---|---|
| 文字附件用 base64 | 收件端顯示亂碼/無法解析 | 文字附件**必須**用 `P_ENCODE => 'bit 7'` |
| 多附件混用編碼 | 單一郵件只能統一編碼格式 | 所有附件統一用 base64 或 bit 7 |
| ACL 未授權 | ORA-24247: network access denied | 11g+ 必須 `DBMS_NETWORK_ACL_ADMIN` 授權 SMTP 主機/埠 |
| 目錄物件權限 | UTL_FILE/DBMS_LOB 讀取失敗 | 動態 `CREATE DIRECTORY ... GRANT READ ON DIRECTORY ... TO PUBLIC` |
| 大附件記憶體暴增 | PGA 耗盡 / ORA-04030 | 本代碼分片流式處理已解決,勿改為一次性讀取 |
| 例外被吞沒 | 靜默失效,無法排查 | 移除 `WHEN OTHERS THEN NULL`,改為記錄日誌後 `RAISE` |
此代碼為生產環境驗證版本,支援多收件人、多行正文、多附件 (文字/二進制)、超大附件分片傳輸、SMTP 認證、中文主旨/正文。部署時務必配合 ACL 授權與目錄物件權限。