首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> Oracle 資料庫 >> Oracle 資料庫基礎

Oracle UTL_SMTP 發送郵件完整實戰代碼 (第五部分:附件處理核心邏輯)

     編輯:Oracle 資料庫基礎

代碼定位

本文為完整郵件發送套件的**第五部分**,專注於**二進制附件分片編碼傳輸核心邏輯**與**主發送流程整合**。前四部分涵蓋:參數定義、字串分割、郵件頭寫入、MIME 邊界處理、文字附件處理、目錄物件管理、檔案路徑解析。

核心邏輯:二進制附件分片 Base64 編碼傳輸


-- 變數說明:
-- 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 = 2016Base64 編碼:3 位元組 → 4 字元。2016 位元組編碼後 = 2688 字元符合 SMTP 單行長度限制 (通常 1000 字元),並能被 54 整除便於分行
L_MAX_LINE_WIDTH = 54Base64 編碼前每行 54 位元組 → 編碼後 72 字元符合 MIME 建議行長度 (76 字元內),避免郵件伺服器截斷
分片迴圈累積 L_DATA每片編碼後立即寫入,不在記憶體累積完整檔案支援**超大附件 (GB 級)**,避免 PGA/OOM
UTL_RAW.CONCAT + UTL_ENCODE.BASE64_ENCODERAW 型別操作,避免字符集轉換干擾二進制資料保證二進制附件 (圖片、PDF、ZIP) 完整性
例外處理區塊任何錯誤先關閉 BFILE,寫入 MIME 結尾邊界,再拋出確保郵件結構完整,收件端能正常解析

主發送流程整合:P_EMAIL 程序


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 denied11g+ 必須 `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 授權與目錄物件權限。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved