seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

Oracle11gR2 Expressで本文UTF8エンコードの添付ファイルつきメール送信用ヘルパをパッケージとして作ってみた。

Oracle11gR2 ExpressのPL/SQLで添付ファイルつきメール送信用パッケージを作ってみました。(本文UTF8エンコード)

[事前準備]

Oracle11gR2 Expressは、こちらから入手できる。
http://www.oracle.com/technetwork/database/express-edition/downloads/index.html?ssSourceSiteId=ocomen

現在(2011/12)では、まだ日本語の案内ページはないようだが、インストールされるExpress EditionはSQLやストアドのエラーメッセージも日本語で出てくるなど、日本語をサポートしているので特に問題はない。

今回の目的では、メール送信のために文字コードの変換やBASE64への変換などを使用することになるが、Express版では以下の点に注意する必要がある。

  1. Express版はデフォルトで各種パッケージの実行権限がつけられていないので、必要に応じてgrantする必要がある。
  2. Express版では、ISO2022_JPなどの一部のキャラクターセットはサポートされていないようである。(SJIS, EUC_JP, UTF8などは問題ない。)


また、Oracle10g以前と11gとの違いにおいては、

  1. UTL_SMTPなどネットワーク接続は初期状態で権限が無いため、事前に接続を許可するようにACLを設定しなければならない。

といった注意点がある。

各種パッケージの権限付与方法について

SYSDBAで接続して、以下のように必要なパッケージに対してgrantする。

すべてのスキーマに対して許可するならpublic、あるいはスキーマを限定しても、どちらでも問題はない。

今回使うのは、以下のパッケージである。

以下、実行結果。

C:\temp>sqlplus / as sysdba
SQL*Plus: Release 11.2.0.2.0 Production on 金 12月 23 23:23:20 2011
Copyright (c) 1982, 2010, Oracle.  All rights reserved.
Oracle Database 11g Express Edition Release 11.2.0.2.0 - Production
に接続されました。
SQL> grant execute on SYS.UTL_ENCODE to PUBLIC;
権限付与が成功しました。
SQL> grant execute on SYS.UTL_RAW to PUBLIC;
権限付与が成功しました。
SQL> grant execute on SYS.UTL_I18N to PUBLIC
権限付与が成功しました。
SQL> grant execute on SYS.UTL_SMTP to PUBLIC;
権限付与が成功しました。
SQL> grant execute on SYS.DBMS_LOB to PUBLIC
権限付与が成功しました。
SQL> grant execute on SYS.DBMS_RANDOM to PUBLIC
権限付与が成功しました。
SQL>

このほか、作成したメールデータなどをPL/SQLで扱うとき、デバッグ用にファイルに出力できると確認しやすいので開発目的でディレクトリ・オブジェクトも作っておく。

また、SQLDeveloperなどでストアドのデバッグをするのであれば、デバック権限も必要となる。
(運用時には必要ない)

create or replace directory TEMP_DIR as 'C:\temp';
grant read, write on directory TEMP_DIR to PUBLIC;

grant execute on SYS.UTL_FILE to PUBLIC;

-- デバッグする場合に必要な権限 (TESTスキーマに対して)
grant debug connect session to TEST;
grant debug any procedure to TEST;
ExpressではISO2022-JPが使えないようだ。

Oracle11g Expressでは変換文字コードとして「ISO2022-JP」が使えないようである。
(ISO2022-JPは、日本語メールの本文によく使われている文字コードである。)

以下のコードを試してみると、

declare
  procedure OutFile(
    p_fileName in varchar2,
    p_message  in varchar2,
    p_charset  in varchar2)
  as
    rawdata raw(100);
    vHandle UTL_FILE.FILE_TYPE;
  begin
    begin
      rawdata := UTL_RAW.CAST_TO_RAW(
        convert(p_message, p_charset)
      );
    exception when others then
      rawdata := UTL_RAW.CAST_TO_RAW(sqlerrm);
    end;
    vHandle := UTL_FILE.FOPEN(
      'TEMP_DIR', -- 予めCREATE DIRECTORYした名前。R/W権限も付与済みのこと。
      p_filename, -- 作成するファイル名、フォルダは含んではならない
      'wb' -- バイナリモードでオープン。(改行コードが自動でつかないようにする)
      );
    UTL_FILE.PUT_RAW(vHandle, rawData);
    UTL_FILE.FCLOSE(vHANDLE);
  end;
begin
  OutFile('sjis.txt', 'こんにちは世界', 'JA16SJISTILDE');
  OutFile('eucjp.txt', 'こんにちは世界', 'JA16EUCTILDE');
  OutFile('iso2022jp.txt', 'こんにちは世界', 'ISO2022-JP'); -- OracleXEでは使えない様子
  OutFile('utf8.txt', 'こんにちは世界', 'AL32UTF8');
end;

先に設定したテスト用のディレクトリ上に、それぞれSJIS, EUC_JP, ISO2022, UTF8でエンコードされたテキストファイルが作成されるのだが、
SJIS, EUP_JP, UTF8では問題なくテキストファイルは作成されているが、
しかし、出力ファイルiso2022jp.txtだけは、「ORA-12703: このキャラクタ・セット変換はサポートされません。」となってしまい、どうやら、使えないらしいと分かる。


なお、ExpressではないOracle 11g R2で試すと、こちらはきちんと変換できる。

もちろん、マニュアルにもConvertで使えるキャラクターセットとして記載がある。
http://docs.oracle.com/cd/B28359_01/server.111/b28298/applocaledata.htm#i636773

対するExpressのマニュアルでは、その記載が見当たらない。
http://docs.oracle.com/cd/E17781_01/install.112/e18802/toc.htm#autoId25


これを見ると、ISO2022が出てこないので、Express版の制約なのかもしれない。
(なお、もともとISO2022はOracle上ではConvertで変換するためだけにしか使えない特殊なキャラクターセット。)

ということで、Oracle 11g Expressでメール送信する場合には、文字コードとして一般的なISO2022-JPは使うことはできない。*1

代わりの方法としてはUTF-8を本文として、転送エンコーディングBASE64化する、などの方法を取ることになるだろう。*2

ネットワーク利用の有効化

Oracle11gでは、ストアドからネットワーク接続するためにはあらかじめ権限を付与しなければならなくなった。


ACLを設定していない状態でUTL_SMTPなどからネットワーク接続を試みると、
ORA-24247: アクセス制御リスト(ACL)によりネットワーク・アクセスが拒否されました
などというエラーにより拒否されることになる。


いくつかのサイトに定型的な利用例があったので、それを、そのまま借用させてもらう。

-- Oracle11gでPL/SQLからUTL_SMTPなどネットワークを使う場合には
-- あらかじめACLで許可しなければならない。

-- http://otndnld.oracle.co.jp/document/products/oracle11g/111/doc_dvd/appdev.111/E05686-02/d_networkacl_adm.htm#BABIGEGG
-- http://docs.oracle.com/cd/B28359_01/appdev.111/b28419/d_networkacl_adm.htm
-- http://osm.utoronto.ca/i/doc/ja/sec_network_services.htm#BHADHHFH
-- http://ameblo.jp/archive-redo-blog/entry-10310716887.html
BEGIN
   DBMS_NETWORK_ACL_ADMIN.CREATE_ACL(
     acl         => 'localmail.xml',
     description => 'local mail acl',
     principal   => 'TEST',
     is_grant    => true,
     privilege   => 'connect'
     );
   DBMS_NETWORK_ACL_ADMIN.ADD_PRIVILEGE(
     acl       => 'localmail.xml',
     principal => 'TEST',
     is_grant  => true,
     privilege => 'resolve'
     );

  DBMS_NETWORK_ACL_ADMIN.ASSIGN_ACL(
     acl  => 'localmail.xml',
     host => 'localhost'
     );
  DBMS_NETWORK_ACL_ADMIN.ASSIGN_ACL(
     acl  => 'localmail.xml',
     host => '127.0.0.1'
     );
END;
/
COMMIT;

これで「TEST」スキーマは、ローカルホスト(localhost/127.0.0.1)へのネットワーク接続が許可されるようになる。
(ACL指定時の各種パラメータにはワイルドカード「*」を使って範囲指定することも可能なようである。)


ACL名は他とかぶらない自由な名前で良く、そこにスキーマと接続先とを定義してゆく形らしい。*3


もちろん、ここでローカルホストを許可しているのは、ローカルホスト上にMTA(Sendmailとか、Apache Jamesとか)があって、そこに接続する、ということを想定しているためである。

もし、異なる場所にメールサーバがあるのであれば、そのサーバのアドレスに対して許可しなければならない。


ACLの削除や設定の確認方法などについては、こちらが参考になると思われる。(2015/4/15追記) http://d.hatena.ne.jp/cyokodog-memo/20120522/1337676161

実験の結果、採用・不採用となったものについての補足。

Oracle 9i時代〜10gでのPL/SQLでのメール送信は過去に何度か試したことがあるが、
Oracle11g Expressと、UTF8+BASE64本文と添付ファイルと長い日本語件名をサポートする組み合わせは今回が初めてである。

いくつか試したことのうち、採用・不採用となったアイディアについての検証の記録も残しておく。

UTL_MAILパッケージ(不採用)

Oracleにはメール送信に使えるパッケージとして、

の二つが用意されている。

UTL_MAILのほうが高機能で少ないパラメータでメール送信することができる、非常にお手軽なパッケージである。

が、しかし、これは、おおざっぱにいって以下の欠点がある。

  1. 日本語が化ける。
  2. 添付ファイルをつけられない。
  3. 細かな制御が無理

結論的には、業務用には使えないと思ってよさそう。

システム管理者宛の英文エラーメッセージを送信するぐらいであれば十分使えると思うし、
その場合はコーディングゼロなので考慮に値すると思う。

UTL_SMTP(採用)

SYS.UTL_SMTPパッケージは、SMTPサーバ(SendmailなどのMTA)に対してTCPコネクションをはって、コマンドを直接やりとりするタイプである。

細かな制御ができる反面、かなり原始的である。

SMTPプロトコルのHELO, SEND FROM, RCPT TO, DATAなどの基本コマンドに対するプロシージャが定義されているので、これを順番に呼び出してゆく形式である。

TCP接続で全部やるよりかは、ずいぶんマシではある。

UTL_ENCODE.MIMEHEADER_ENCODEファンクション (不採用)

UTL_SMTPで送信する場合、たとえばメールヘッダのSubject(件名)などに日本語がある場合は、MIME HEADERのエンコードが必要になる。

具体的には「=?UTF-8?B?xxxxxx?=」みたいな形で7ビットセーフなASCII文字で1行あたり78文字以内の文字列にエンコードしてやる必要がある。

これは件名だけでなく、メールアドレスの送信者名や受信者名の表記でも同様な処置が必要となる。

これは、けっこうめんどくさいのであるが、UTL_ENCODEパッケージの「MIMEHEADER_ENCODE」を使うと,これをやってくれるのである。

が、これも欠点があって実際試すとわかるのだが、おおざっぱにいうと、

  • 分割したときに先頭にタブが入らないので継続行として見てくれない。
  • 分割する位置がおかしいので長い件名は文字化ける可能性が高い

ということで、これも実際のところ、あんまり使えないような感じである。

なので、今回は、これは使わない。

13文字以内ぐらいの短い件名であれば、たぶん、これで問題なく使えそうではある。

UTL_I18N, UTL_RAW, UTL_ENCODEによるMIMEヘッダのエンコード処理 (採用)

MIMEヘッダのエンコード処理を自前で実現するには、おおざっぱに言って以下のような処理となる。

  1. 今回はUTF8+BASE64なので、UTF8一文字3バイトとして、それがBASE64に変換されて、エンコード後は一文字4バイトの長さとなる。
  2. UTF8への変換はUTL_I18Nパッケージで、NVARCHAR2/VARCHAR2どちらでも変換可能。BASE64への変換はUTL_ENCODEパッケージを使う。
  3. エンコードの前後には「=?UTF-8?B?」と「?=」というマーカーでエンコードされていることを示す必要がある。
  4. ヘッダの「Subject: 」と上記マーカー分とエンコードされた文字列の合計が78文字以内となるように区切りのよいところで折り返す
  5. 以上のことから、エンコードは余裕を見て12文字ごとに区切ってエンコードする。(1文字3バイト以上になるサロゲートペアなケースは考慮していない。)
  6. 継続行はタブで始まる。タブが始まる行は前行の続きとみなされる。
  7. 改行はCRLF

これを実装すれば良い。

DECLARE
  -- UTF8のOracle(NLS)表現
  UTF8 constant nvarchar2(250) := 'AL32UTF8';
   
  -- 改行コード
  CRLF CONSTANT VARCHAR2(2) := chr(13) || chr(10);

  -- タブコード
  TAB CONSTANT VARCHAR2(1) := chr(9);

  /**
   * 引数に指定した非ASCII文字を含む文字列を、UTF8/BASE64でエンコードし
   * 80桁以内に収まるように折り返した複数行形式のデータに変換して返す.
   * 返されたRAW型はASCII文字だけを含んでおり、メールヘッダの値として使用できる.
   * 末尾に改行コードを付与するか指定することが可能.
   */
  FUNCTION MAKE_HEADER_VALUE(
    p_value NVARCHAR2,
    p_CRLF BOOLEAN := TRUE
  ) RETURN RAW IS
    -- ヘッダ部のUTF-8/BASE64エンコード開始マーカー
    ENCMARK constant varchar2(10) := '=?UTF-8?B?';
  
    -- ヘッダ部のエンコード終了マーカー
    ENCMARK_EN constant varchar2(2) := '?=';

    -- 変換結果 (32KiBが最大)
    ret RAW(32767);

  BEGIN
    DECLARE
      -- エンコード後のヘッダ行を1行80byteに納めるための分割文字数
      -- UTF8なので日本語は3バイト程度に膨らむことを想定 (サロゲートペアは考慮外)
      -- BASE64なので1.333倍に膨らむことを想定
      CHUNKSIZE CONSTANT PLS_INTEGER := 12;

      -- 入力データ(p_value)の文字数
      MXLEN CONSTANT PLS_INTEGER := LENGTH(p_value);

      -- 現在処理中の桁      
      pos PLS_INTEGER := 1;
      
      -- 1行ごとに処理する対象文字列の切り出しバッファ
      linechunk NVARCHAR2(12);
      
      -- BASE64エンコード処理用バッファ
      rawdata RAW(80);

    BEGIN
      LOOP
        -- すべて処理したらループ終了
        EXIT WHEN pos > MXLEN;
        
        -- 分割後であれば改行+タブ
        IF pos > 1 THEN
          ret := UTL_RAW.CONCAT(
            ret, 
            UTL_I18N.STRING_TO_RAW(CRLF || TAB, UTF8)
            );
        END IF;

        -- 分割文字列の取得
        linechunk := substr(p_value, pos, CHUNKSIZE);
        
        -- UTF8に変換
        rawdata := UTL_I18N.STRING_TO_RAW(linechunk, UTF8);
        IF rawdata IS NULL THEN
          RAISE_APPLICATION_ERROR(-20101, 'UTF8に変換できません。', FALSE);
        END IF;
  
        -- 出力
        ret := UTL_RAW.CONCAT(
          ret, 
          UTL_I18N.STRING_TO_RAW(ENCMARK, UTF8),
          UTL_ENCODE.BASE64_ENCODE(rawdata),
          UTL_I18N.STRING_TO_RAW(ENCMARK_EN, UTF8)
          );

        -- 次の位置へ進む
        pos := pos + chunksize;
      END LOOP;

      IF p_CRLF THEN
        -- 改行終端
        ret := UTL_RAW.CONCAT(
          ret, 
          UTL_I18N.STRING_TO_RAW(CRLF, UTF8)
          );
      END IF;
    END;
    RETURN ret;
  END;

BEGIN
  DECLARE
    vHANDLE UTL_FILE.FILE_TYPE;

  BEGIN
    vHANDLE := UTL_FILE.FOPEN('TEMP_DIR', 'hello.txt', 'wb');
    UTL_FILE.PUT_RAW(
      vHANDLE,
      MAKE_HEADER_VALUE('Hello, World! こんにちは世界!!')
      );
    UTL_FILE.FCLOSE(vHANDLE);

  EXCEPTION WHEN OTHERS THEN
    UTL_FILE.FCLOSE_ALL;
  END;
END;

これを実行すると、C:\TEMP\hello.txtに

=?UTF-8?B?SGVsbG8sIFdvcmxk?=
	=?UTF-8?B?ISDjgZPjgpPjgavjgaHjga/kuJbnlYwhIQ==?=

というような結果が得られる。

これを、たとえばThunderbirdで受信した適当なメールをファイルとして保存して、その*.emlファイルをテキストエディタで開いて
「Subject:」の部分を上記で置き換えてThunderbirdで開きなおすなどすることで、上記エンコードの中身を確認することができる。

添付ファイルとして一時BLOBをバッファとして使う。(激しく採用)

今回作成するメール送信ヘルパパッケージではメール送信時に複数個の添付ファイルをつけることができるようにするが、
いろいろ悩んだ末に、単純に引数に添付ファイルにしたいバイナリデータを渡すことにした。


しかし、PL/SQLでバイナリを扱うのは通常、RAW型であるが、RAW型は32Kバイトまでの制限がある
テーブル抽出結果をUTF8エンコードされたCSVファイルで送信するとか、そうゆう用途で添付ファイルを使うとなると、32K制限は、いささか小さすぎる。

PL/SQLは一応言語なわけで、画像やらデータ抽出やらで32K以上のバイナリデータが構築するケースは少なくないはずだから、なにか標準パッケージで、バイナリバッファの機能をもつものがあるはずだ、と探してみた。しかし、標準パッケージに、それらしいのが見当たらない…。

おかしい。そんなはずはない、と今度はバイナリを扱う例をネットで探したら、ようやく謎が解けました。


特別なものは何も必要はありません。


OraclePL/SQLで32Kを超えるバイナリデータを構築したい場合は、SQL99標準でも定義されている、ただのBLOB(Binary Large OBject)型で良いのでした。*4


ということで、BLOBを添付ファイルのデータホルダとして使うこととした。

BLOBはテーブルのカラムとして存在しなくても一時BLOBとして初期化して使うことができる。

PL/SQL上でBLOB変数を定義し、DMBS_LOBパッケージの「CREATETEMPORARY」で一時BLOBとして初期化してやることで、一時表領域上にBLOBを割り当ててくれる。

このようにして初期化された一時BLOBは、普通のBLOBと同じように扱えるうえ、呼出しスコープ、もしくはセッションスコープで自動的に破棄されることも保証されているので、事実上、容量無制限な便利なByteBufferとして使えるわけである。*5


以下、メールの添付ファイルでの出力の実験を兼ねて、BLOBで32Kを超えるバイナリデータを作成し、それをBASE64エンコードしてファイル出力してみる例。

declare
 -- 32Kを超えるバイナリデータをBLOB上に作成する
 procedure generate_message(lb in out nocopy blob, cnt number) is
   mes varchar2(4096);
   rd raw(4096);
 begin
   for idx in 1..cnt
   loop
    mes := 'メッセージNo. ' || to_char(idx) || chr(10);
    rd := UTL_I18N.STRING_TO_RAW(mes, 'UTF8');
    DBMS_LOB.WRITEAPPEND(lb, UTL_RAW.LENGTH(rd), rd);
   end loop;
 end;
 
 -- BLOBから48バイトづつ取り出しBASE64に変換し
 -- 1行64文字でファイルに出力する
 procedure write_file(lb in out nocopy blob) is
  vHandle UTL_FILE.FILE_TYPE;
  amount BINARY_INTEGER := 768; -- 48の倍数 (3と4で割り切れる数の1行単位);
  pos pls_integer := 1;
  blob_len pls_integer := dbms_lob.getlength(lb);
  buf raw(1152); -- BASE64変換後のサイズ(読み込みバイトの1.5倍)
 begin
  vHANDLE := UTL_FILE.FOPEN('TEMP_DIR', 'data.txt', 'wb');
  
  -- 48バイトで端数なしで出力できるBASE64の1行の最大桁.
  -- その倍数単位のバッファでループを回す.
  WHILE pos < blob_len
  LOOP
    DBMS_LOB.READ(lb, amount, pos, buf);
    UTL_FILE.PUT_RAW(
      vHANDLE,
      UTL_ENCODE.BASE64_ENCODE(buf), -- 64桁毎に改行コードが入る
      TRUE
      );
    buf := NULL;
    pos := pos + amount;
  END LOOP;  
  
  UTL_FILE.FCLOSE(vHANDLE);

  EXCEPTION WHEN OTHERS THEN
   UTL_FILE.FCLOSE(vHANDLE);
   RAISE;
 end;
 
begin
  declare
   lb blob;
   st timestamp;
   en timestamp;
  begin
   -- 一時BLOBを構築、寿命を呼び出しスコープに設定
   DBMS_LOB.CREATETEMPORARY(lb, TRUE, DBMS_LOB.CALL);

   -- 32Kを超えるデータを作成
   generate_message(lb, 23456);

   -- 長さの検証
   st := systimestamp;
   dbms_output.put_line('len=' || dbms_lob.getlength(lb) || '/' || st);

   -- ファイルにBASE64として書き出し
   write_file(lb);
   en := systimestamp;
   dbms_output.put_line(en || '/span=' || (en - st));

   -- 一時LOBの破棄
   DBMS_LOB.FREETEMPORARY(lb);
  end;
end;

あとは、通常のBLOBのように、この一時BLOBに対してデータを追記していって、メール送信する段階になったら、添付ファイルデータとして、このBLOBを渡せるようにしておく。

BLOBにしておくことで単一変数として扱えるので、作るのも引き渡すのも、いろいろ楽になった。


ちなみに、BLOBからバッファからの取り出しは、ならべくバッファサイズを大きくした方が効率が良いようである。

この例では768バイトとしているが、これは48バイトでBASE64変換後に64桁となり、ちょうど改行が入るため、その48の倍数にあたる数値である。

ORACLEUTL_ENCODE.BASE64_ENCODEでは、マニュアルに明確には記載されていないのだが64桁で改行することにしているようだ。*6

(つまり、一行は48バイトのため、48バイト単位に読み取る必要があり、半端な数だと改行がおかしくなる。)

以上により、最低48バイトサイズのバッファとなるが、48バイトの場合はループ回数が多くのなるためか、明らかに実行時間が大幅に増えてしまう。
よって、ループ回数が少なくなるようにするのがポイントのようである。

添付ファイル名のエンコーディング

添付ファイル名はMIMEヘッダで「Content-Disposition: attachment; filename=xxxx」のように指定する。

ただしファイル名に日本語のように7ビットセーフでない文字コードを使う場合にはMIMEヘッダのエンコードが必要となる。

最近では、この添付ファイル名のエンコード仕様はRFC2231で定められていて、
「filename*N*=xxxx」の形式の複数行となり、継続する場合は末尾はセミコロンとなる。

エンコード文字コード名の識別子につづいて16進数でエンコードされた文字が続く。

以下、その例。

Content-Disposition: attachment;
	filename*0*=UTF-8''%E6%B7%BB%E4%BB%98%E3%83%95%E3%82%A1%E3%82%A4%E3;
	filename*1*=%83%AB%E3%83%BB%E3%83%80%E3%83%9F%E3%83%BC%E3%83%87%E3%83;
	filename*2*=%BC%E3%82%BF%20%E2%84%96%31%2E%74%78%74

ただし、この形式はOutlook2010などでは、まだ認識できない。

Outlookにも日本語ファイル名を認識させるには、Content-Type側に「Name=xxxx」形式のパラメータを追加する必要がある。

こちらは、件名やメールアドレスの表記と同じように、MIMEヘッダのエンコーディングと同じルールである。

以下、その例。

Content-Type: text/plain; charset=UTF-8;
	name="=?UTF-8?B?5re75LuY44OV44Kh44Kk44Or44O744OA44Of44O844OH44O8?=
	=?UTF-8?B?44K/IOKEljEudHh0?="

なので、Thunderbirdなどのメールクライアントは、どちらの形式も出力しているようである。

よって、本プログラムでも両方出すこととする。

このあたりの事情は、以下のサイトが詳しかった。

メール送信パッケージ

ようやく本題。

特筆すべきと思われる点はすでに述べたとおりで、あとは淡々とくみ上げるのみである。

読み返したときに疑問になると思われる部分についてはコード中にもコメントを入れている。

なお、このソースコード・テストコード一式は、gist: 1519233からダウンロード可能である。

パッケージ宣言部
CREATE OR REPLACE PACKAGE MAILHELPER AS 

  /**
   * SMTP接続先サーバ
   */
  VC_MAILHOST VARCHAR2(64) := '127.0.0.1';
  
  /**
   * SMTP接続先ポート
   */
  VC_MAILPORT NUMBER := 25;

  /**
   * メールアドレスの情報
   */
  TYPE MAIL_ADDRESS IS RECORD(
    
    /**
     * アドレスの表記、不要ならばNULL
     */
    DESCRIPTION NVARCHAR2(64),
    
    /**
     * アドレス.
     * (RFCでは、メールアドレスは全体で256文字)
     */
    EMAIL VARCHAR2(256)
  );
  
  /**
   * メールアドレス情報のリスト
   */
  TYPE MAIL_ADDRESS_LIST IS TABLE OF MAIL_ADDRESS INDEX BY binary_integer; 

  /**
   * 添付ファイル情報
   */
  TYPE ATTACHED_FILE IS RECORD(

    /**
     * 添付ファイル名.
     * UTF8/16進数でのエンコードで出力される.
     */
    FILE_NAME NVARCHAR2(256),
    
    /**
     * コンテンツタイプ。
     * text/csvとか、application/octet-streamなど。
     */
    CONTENT_TYPE VARCHAR2(48),
    
    /**
     * 添付ファイルとするバイナリデータ
     */
    CONTENT BLOB
  );
  
  /**
   * 複数添付ファイル
   */
  TYPE ATTACHED_FILES IS TABLE OF ATTACHED_FILE INDEX BY binary_integer;


  /**
   * 添付なしのメール送信を行う。
   * @param p_from 送信元メールアドレス
   * @param p_to 送信先メールアドレス
   * @param p_subject 件名
   * @param p_message 本文
   */
  PROCEDURE SENDMAIL(
    p_from NVARCHAR2,
    p_to NVARCHAR2,
    p_subject NVARCHAR2,
    p_message NVARCHAR2
    );

  /**
   * 添付なしのメール送信を行う。
   * @param p_from 送信元メールアドレス情報
   * @param p_to 送信先メールアドレス情報のリスト
   * @param p_subject 件名
   * @param p_message 本文
   */
  PROCEDURE SENDMAIL(
    p_from MAIL_ADDRESS,
    p_to_list MAIL_ADDRESS_LIST,
    p_subject NVARCHAR2,
    p_message NVARCHAR2
    );

  /**
   * 添付ファイルありのメール送信を行う.
   * @param p_from 送信元メールアドレス
   * @param p_to 送信先メールアドレス
   * @param p_subject 件名
   * @param p_message 本文
   * @param p_file 添付ファイル情報
   */
  PROCEDURE SENDMAIL(
    p_from NVARCHAR2,
    p_to NVARCHAR2,
    p_subject NVARCHAR2,
    p_message NVARCHAR2,
    p_file ATTACHED_FILE
    );

  /**
   * 添付ファイルありのメール送信を行う.
   * @param p_from 送信元メールアドレス情報
   * @param p_to_list 送信先メールアドレス情報のリスト
   * @param p_subject 件名
   * @param p_message 本文
   * @param p_files 添付ファイル情報
   */
  PROCEDURE SENDMAIL(
    p_from MAIL_ADDRESS,
    p_to_list MAIL_ADDRESS_LIST,
    p_subject NVARCHAR2,
    p_message NVARCHAR2,
    p_files ATTACHED_FILES
    );

END MAILHELPER;
/
パッケージボディ
CREATE OR REPLACE package body MAILHELPER as

  -- UTF8のOracle表現
  UTF8 constant nvarchar2(250) := 'AL32UTF8';
   
  -- 改行コード
  CRLF CONSTANT VARCHAR2(2) := chr(13) || chr(10);

  -- タブコード
  TAB CONSTANT VARCHAR2(1) := chr(9);

  -- 内部用、UTL_SMTPのハンドル
  vHANDLE UTL_SMTP.CONNECTION;

  /**
   * 引数に指定した非ASCII文字を含む文字列を、UTF8/BASE64でエンコードし
   * 80桁以内に収まるように折り返した複数行形式のデータに変換して返す.
   * 返されたRAW型はASCII文字だけを含んでおり、メールヘッダの値として使用できる.
   * 末尾に改行コードを付与するか指定することが可能.
   */
  FUNCTION MAKE_HEADER_VALUE(
    p_value NVARCHAR2,
    p_CRLF BOOLEAN := TRUE
  ) RETURN RAW IS
    -- ヘッダ部のUTF-8/BASE64エンコード開始マーカー
    ENCMARK constant varchar2(10) := '=?UTF-8?B?';
  
    -- ヘッダ部のエンコード終了マーカー
    ENCMARK_EN constant varchar2(2) := '?=';

    -- 変換結果 (32KiBが最大)
    ret RAW(32767);

  BEGIN
    DECLARE
      -- エンコード後のヘッダ行を1行80byteに納めるための分割文字数
      -- UTF8なので日本語は3バイト程度に膨らむことを想定 (サロゲートペアは考慮外)
      -- BASE64なので1.333倍に膨らむことを想定
      CHUNKSIZE CONSTANT PLS_INTEGER := 12;

      -- 入力データ(p_value)の文字数
      MXLEN CONSTANT PLS_INTEGER := LENGTH(p_value);

      -- 現在処理中の桁      
      pos PLS_INTEGER := 1;
      
      -- 1行ごとに処理する対象文字列の切り出しバッファ
      linechunk NVARCHAR2(12);
      
      -- BASE64エンコード処理用バッファ
      rawdata RAW(80);

    BEGIN
      LOOP
        -- すべて処理したらループ終了
        EXIT WHEN pos > MXLEN;
        
        -- 分割後であれば改行+タブ
        IF pos > 1 THEN
          ret := UTL_RAW.CONCAT(
            ret, 
            UTL_I18N.STRING_TO_RAW(CRLF || TAB, UTF8)
            );
        END IF;

        -- 分割文字列の取得
        linechunk := substr(p_value, pos, CHUNKSIZE);
        
        -- UTF8に変換
        rawdata := UTL_I18N.STRING_TO_RAW(linechunk, UTF8);
        IF rawdata IS NULL THEN
          RAISE_APPLICATION_ERROR(-20101, 'UTF8に変換できません。', FALSE);
        END IF;
  
        -- 出力
        ret := UTL_RAW.CONCAT(
          ret, 
          UTL_I18N.STRING_TO_RAW(ENCMARK, UTF8),
          UTL_ENCODE.BASE64_ENCODE(rawdata),
          UTL_I18N.STRING_TO_RAW(ENCMARK_EN, UTF8)
          );

        -- 次の位置へ進む
        pos := pos + chunksize;
      END LOOP;

      IF p_CRLF THEN
        -- 改行終端
        ret := UTL_RAW.CONCAT(
          ret, 
          UTL_I18N.STRING_TO_RAW(CRLF, UTF8)
          );
      END IF;
    END;
    RETURN ret;
  END;

  /**
   * 添付ファイルのファイル名のエンコードをRFC2231により行う.
   * 「filename*N*=xxxx」の形式の複数行となり、継続する場合は末尾はセミコロンとなる。
   * エンコードは文字コード名の識別子につづいて16進数でエンコードされた文字が続く.
   * (例) filename*0*=UTF-8''%E6%96%B0%E8%A6
   * http://www.asahi-net.or.jp/~BD9Y-KTU/htmlrel_f/dtd_f/rfc_f/rfc2231j.html
   * http://adiary.blog.abk.nu/0253
   * @param filename 添付ファイル名
   * @return UTF8の16進数表現でエンコードされたファイル名表現
   */
  FUNCTION MAKE_ATTACHED_FILE_NAME(filename NVARCHAR2) RETURN RAW AS
    buf RAW(32760);
    c RAW(1);

    pos PLS_INTEGER := 1;
    mxlen PLS_INTEGER;
    
    linebuf VARCHAR2(80) := 'UTF-8''''';
    TYPE t_lines IS TABLE OF VARCHAR2(80) INDEX BY binary_integer;
    lines t_lines;

  BEGIN
    IF filename IS NULL THEN
      RETURN NULL;
    END IF;

    -- ファイル名をUTF8のバイナリに変換  
    buf := UTL_I18N.STRING_TO_RAW(filename, UTF8);
    
    -- バイナリから「16進数」の文字列に変換。
    -- 54文字を超えた場合は次の行に折り返す.
    mxlen := UTL_RAW.LENGTH(buf);
    LOOP
      EXIT WHEN pos > mxlen;
      IF LENGTH(linebuf) > 54 THEN
        lines(lines.COUNT) := linebuf || ';';
        linebuf := NULL;
      END IF;
      c := utl_raw.substr(buf, pos, 1);
      linebuf := linebuf || '%' || rawtohex(c);
      pos := pos + 1;
    END LOOP;
    lines(lines.COUNT) := linebuf;

    -- ヘッダを構築してバイナリとして構築する.    
    buf := NULL;
    FOR idx IN lines.FIRST .. lines.LAST
    LOOP
      IF lines.COUNT > 1 THEN
        linebuf := TAB || 'filename*' || idx || '*=' || lines(idx);
      ELSE
        linebuf := TAB || 'filename*=' || lines(0);
      END IF;
      buf := UTL_RAW.CONCAT(
        buf,
        UTL_RAW.CAST_TO_RAW(linebuf),
        UTL_RAW.CAST_TO_RAW(CRLF)
        );
    END LOOP;
    RETURN buf;
  END;
  
  /**
   * 内部用.
   * From/Toなどのメールアドレスをもつヘッダを出力する。
   * @param p_header ヘッダ
   * @param p_to_list メールアドレスのリスト
   */
  PROCEDURE WRITE_ADDRESS(
    p_header VARCHAR2,
    p_address_list MAIL_ADDRESS_LIST
  ) AS
  BEGIN
    -- ヘッダキー
    UTL_SMTP.WRITE_RAW_DATA(
      vHANDLE,
      UTL_RAW.CONCAT(
        UTL_RAW.CAST_TO_RAW(p_header)
        )
      );
    
    -- メールアドレスの個数分繰り返す
    FOR idx IN p_address_list.FIRST .. p_address_list.LAST
    LOOP
      IF idx > 1 THEN
        UTL_SMTP.WRITE_RAW_DATA(
          vHANDLE,
          UTL_RAW.CONCAT(
            UTL_I18N.STRING_TO_RAW(', '),
            UTL_RAW.CAST_TO_RAW(CRLF),
            UTL_RAW.CAST_TO_RAW(TAB)
            )
        );
      END IF;
      
      -- メールの表記が指定されていればUTF8+BASE64のMIMEヘッダエンコードで
      -- 出力する。(必要に応じて継続行にする。)
      DECLARE
        description NVARCHAR2(60) := p_address_list(idx).DESCRIPTION;
        mailaddr VARCHAR2(256) := p_address_list(idx).EMAIL;
        nameraw RAW(32767);
        linelen PLS_INTEGER;
      BEGIN
        IF description IS NOT NULL THEN
          -- エンコードしたアドレス表記を出力
          nameraw := MAKE_HEADER_VALUE(description, FALSE);
          UTL_SMTP.WRITE_RAW_DATA(
            vHANDLE,
            nameraw
          );
          -- アドレス部をつづけて出力できるか、継続行にするか判定
          linelen := UTL_RAW.LENGTH(nameraw) + length(mailaddr);
          IF linelen >= 70 THEN -- アドレス前後の山括弧とヘッダ名を含まず
            -- EMAILを出力するに桁数が足りない場合は継続行とする.
            UTL_SMTP.WRITE_RAW_DATA(
              vHANDLE,
              UTL_RAW.CONCAT(
                UTL_RAW.CAST_TO_RAW(CRLF),
                UTL_RAW.CAST_TO_RAW(TAB)
                )
            );
          ELSE
            -- つづけてアドレス出力可能
            UTL_SMTP.WRITE_RAW_DATA(
              vHANDLE,
              UTL_RAW.CAST_TO_RAW(' ')
            );
          END IF;
        END IF;
      END;
    
      -- メールアドレスの出力。
      -- 前後には山括弧をつける。
      UTL_SMTP.WRITE_RAW_DATA(
        vHANDLE,
        UTL_RAW.CONCAT(
          UTL_I18N.STRING_TO_RAW('<'),
          UTL_I18N.STRING_TO_RAW(p_address_list(idx).EMAIL),
          UTL_I18N.STRING_TO_RAW('>')
          )
        );
    END LOOP;

    -- ヘッダ終端
    UTL_SMTP.WRITE_RAW_DATA(
      vHANDLE,
      UTL_RAW.CAST_TO_RAW(CRLF)
    );
  END;

  /**
   * 内部用.
   * UTL_SMTPメール送信でハンドルを取得、送信先と基本ヘッダの設定までを行う.
   * @param p_from 送信元メールアドレス
   * @para, p_to 送信先メールアドレス
   * @param p_subject 件名
   */
  PROCEDURE OPEN_MAIL(
    p_from MAIL_ADDRESS,
    p_to_list MAIL_ADDRESS_LIST,
    p_subject NVARCHAR2
  ) AS
    p_from_list MAIL_ADDRESS_LIST;
  BEGIN
    p_from_list(1) := p_from;
    
    -- SMTPサーバへの接続
    vHANDLE := UTL_SMTP.OPEN_CONNECTION(VC_MAILHOST, VC_MAILPORT);
    BEGIN
      UTL_SMTP.HELO(vHANDLE, VC_MAILHOST);
  
      -- MAIL FROM
      UTL_SMTP.MAIL(vHANDLE, '<' || p_from.EMAIL || '>');
      
      -- RCPT TOを宛先ごとに繰り返す
      FOR idx IN p_to_list.FIRST .. p_to_list.LAST
      LOOP
        UTL_SMTP.RCPT(vHANDLE, '<' || p_to_list(idx).EMAIL || '>');
      END LOOP;
      
      -- メールデータ開始
      UTL_SMTP.OPEN_DATA(vHANDLE);

      -- FROMヘッダ
      WRITE_ADDRESS('From: ', p_from_list);
  
      -- TOヘッダ
      WRITE_ADDRESS('To: ', p_to_list);
  
      -- 件名ヘッダ
      UTL_SMTP.WRITE_RAW_DATA(
        vHANDLE,
        UTL_RAW.CAST_TO_RAW('Subject: ')
        );
      UTL_SMTP.WRITE_RAW_DATA(
        vHANDLE,
        MAKE_HEADER_VALUE(p_subject)
        );

    EXCEPTION WHEN OTHERS THEN
      BEGIN
        -- コネクションをクローズする.
        UTL_SMTP.QUIT(vHANDLE);
      EXCEPTION WHEN OTHERS THEN
        -- クローズの失敗は無視する.
        NULL;
      END;
      vHANDLE := NULL;
      RAISE;
    END;
  END;
  
  /**
   * メールオープン後の状態において、メール送信を閉じる.
   */
  PROCEDURE QUIT_MAIL(p_abort IN BOOLEAN) IS
  BEGIN
    IF vHANDLE.HOST IS NOT NULL THEN
      IF p_abort THEN
        BEGIN
          -- メールデータ破棄
          UTL_SMTP.RSET(vHANDLE);
        EXCEPTION WHEN OTHERS THEN
          -- 破棄の失敗は無視する.
          NULL;
        END;
      ELSE
          -- メールデータ完了
          UTL_SMTP.CLOSE_DATA(vHANDLE); 
      END IF;
      
      BEGIN      
        -- コネクションをクローズする.
        UTL_SMTP.QUIT(vHANDLE);
      EXCEPTION WHEN OTHERS THEN
        -- クローズの失敗は無視する.
        NULL;
      END;

      -- ハンドルをクリアする.
      vHANDLE := NULL;
    END IF;  
  END;

  
  /**
   * 添付なしのメール送信を行う。
   * @param p_from 送信元メールアドレス
   * @param p_to 送信先メールアドレス
   * @param p_subject 件名
   * @param p_message 本文
   */
  PROCEDURE SENDMAIL(
    p_from NVARCHAR2,
    p_to NVARCHAR2,
    p_subject NVARCHAR2,
    p_message NVARCHAR2
  ) AS
    p_to_list MAIL_ADDRESS_LIST;
    v_mail_from MAIL_ADDRESS;
    v_mail_to   MAIL_ADDRESS;
  BEGIN
    v_mail_from.EMAIL := p_from;
    v_mail_to.EMAIL := p_to;
    p_to_list(1) := v_mail_to;
    SENDMAIL(v_mail_from, p_to_list, p_subject, p_message);
  END;
  
  /**
   * 添付なしのメール送信を行う。
   * @param p_from 送信元メールアドレス
   * @param p_to 送信先メールアドレスのリスト
   * @param p_subject 件名
   * @param p_message 本文
   */
  PROCEDURE SENDMAIL(
    p_from MAIL_ADDRESS,
    p_to_list MAIL_ADDRESS_LIST,
    p_subject NVARCHAR2,
    p_message NVARCHAR2
  ) IS
  BEGIN
    OPEN_MAIL(p_from, p_to_list, p_subject);
    BEGIN
      UTL_SMTP.WRITE_RAW_DATA(
        vHANDLE,
        UTL_RAW.CONCAT(
          UTL_RAW.CAST_TO_RAW('MIME-version: 1.0'),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW('Content-type: text/plain; '),
          UTL_RAW.CAST_TO_RAW('charset=UTF-8'),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW('Content-transfer-encoding: BASE64'),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_ENCODE.BASE64_ENCODE(
            UTL_I18N.STRING_TO_RAW(p_message, UTF8)
            )
          )
        );
  
      QUIT_MAIL(p_abort => false);
   
    EXCEPTION WHEN OTHERS THEN
      QUIT_MAIL(p_abort => true);
      RAISE_APPLICATION_ERROR(-20120, 'メール送信に失敗しました。' || SQLERRM, TRUE);
    END;
  END;
  
  
  /**
   * 1個の添付ファイルありのメール送信を行う.
   * @param p_from 送信元メールアドレス
   * @param p_to 送信先メールアドレス
   * @param p_subject 件名
   * @param p_message 本文
   * @param p_file 添付ファイル情報
   */
  PROCEDURE SENDMAIL(
    p_from NVARCHAR2,
    p_to NVARCHAR2,
    p_subject NVARCHAR2,
    p_message NVARCHAR2,
    p_file ATTACHED_FILE
  ) AS
    p_files ATTACHED_FILES;
    p_to_list MAIL_ADDRESS_LIST;
    v_mail_from MAIL_ADDRESS;
    v_mail_to MAIL_ADDRESS;
  BEGIN
    v_mail_from.EMAIL := p_from;
    v_mail_to.EMAIL := p_to;
    p_to_list(1) := v_mail_to;
    p_files(p_files.COUNT + 1) := p_file;
    SENDMAIL(v_mail_from, p_to_list, p_subject, p_message, p_files);
  END;

  /**
   * 添付ファイルありのメール送信を行う.
   * ただし、p_filesが空である場合は添付なしで送信される.
   * @param p_from 送信元メールアドレス
   * @param p_to_list 送信先メールアドレス
   * @param p_subject 件名
   * @param p_message 本文
   * @param p_files 添付ファイル情報のコレクション、空の場合は添付なし
   */
  PROCEDURE SENDMAIL(
    p_from MAIL_ADDRESS,
    p_to_list MAIL_ADDRESS_LIST,
    p_subject NVARCHAR2,
    p_message NVARCHAR2,
    p_files ATTACHED_FILES
  ) AS
    SEPARATOR CONSTANT varchar2(50) := '----_' || DBMS_RANDOM.STRING('A', 45);
  BEGIN
    IF p_files.FIRST IS NULL THEN
      -- 添付ファイルなしの場合
      SENDMAIL(p_from, p_to_list, p_subject, p_message);
      RETURN;
    END IF;

    OPEN_MAIL(p_from, p_to_list, p_subject);
    BEGIN
      -- ヘッダ部の作成 (MIME)
      UTL_SMTP.WRITE_RAW_DATA(
        vHANDLE,
        UTL_RAW.CONCAT(
          UTL_RAW.CAST_TO_RAW('MIME-version: 1.0'),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW('Content-type: multipart/mixed;'),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW(' boundary=' || SEPARATOR),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW(CRLF)
          )
        );
  
      -- 本文の作成
      UTL_SMTP.WRITE_RAW_DATA(
        vHANDLE,
        UTL_RAW.CONCAT(
          UTL_RAW.CAST_TO_RAW('--' || SEPARATOR),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW('Content-Type: text/plain; '),
          UTL_RAW.CAST_TO_RAW('Charset=UTF-8'),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW('Content-transfer-encoding: BASE64'),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_RAW.CAST_TO_RAW(CRLF),
          UTL_ENCODE.BASE64_ENCODE(
            UTL_I18N.STRING_TO_RAW(p_message, UTF8)
            ),
          UTL_RAW.CAST_TO_RAW(CRLF)
          )
        );
      
      -- すべての添付ファイルを出力するループ
      FOR idx IN p_files.FIRST .. p_files.LAST
      LOOP
        DECLARE
          -- 対象添付ファイル
          p_file ATTACHED_FILE := p_files(idx);
        BEGIN
          -- 添付ファイル開始
          UTL_SMTP.WRITE_RAW_DATA(
            vHANDLE,
            UTL_RAW.CONCAT(
              UTL_RAW.CAST_TO_RAW('--' || SEPARATOR),
              UTL_RAW.CAST_TO_RAW(CRLF),
  
              -- 添付ファイルのコンテンツタイプ
              UTL_RAW.CAST_TO_RAW('Content-Type: '
                || p_file.CONTENT_TYPE || ';' || CRLF || TAB || 'name="'),
              -- MIMEによるファイル名の指定 (Outlookなどは、こちらで認識する。)
              MAKE_HEADER_VALUE(p_file.FILE_NAME, FALSE),
              UTL_RAW.CAST_TO_RAW('"' || CRLF),
  
              -- 添付ファイルのファイル名
              -- Thunderbirdなどは、こちらで認識する。
              UTL_RAW.CAST_TO_RAW('Content-Disposition: attachment;'),
              UTL_RAW.CAST_TO_RAW(CRLF),
              MAKE_ATTACHED_FILE_NAME(p_file.FILE_NAME),
  
              -- 添付ファイルデータはBASE64によるエンコードされていることを示す.
              UTL_RAW.CAST_TO_RAW('Content-transfer-encoding: BASE64'),
              UTL_RAW.CAST_TO_RAW(CRLF),
              -- MIMEヘッダ終了
              UTL_RAW.CAST_TO_RAW(CRLF)
              )
            );
            
          -- 添付ファイル出力
          DECLARE
            amount BINARY_INTEGER := 768; -- 48の倍数(3と4で割り切れる数, 48バイト=64桁);
            pos pls_integer := 1;
            blob_len pls_integer := dbms_lob.getlength(p_file.CONTENT);
            buf raw(1152); -- BASE64変換後のサイズ(余裕を見て読み込みバイトの1.5倍を確保)
          BEGIN
            WHILE pos < blob_len
            LOOP
              DBMS_LOB.READ(p_file.CONTENT, amount, pos, buf);
              UTL_SMTP.WRITE_RAW_DATA(
                vHANDLE,
                UTL_ENCODE.BASE64_ENCODE(buf) -- 64文字ごとに自動で改行コードが入る
                );
              buf := NULL;
              pos := pos + amount;
            END LOOP;  
          END;
        END;
  
        -- 添付ファイル終端
        UTL_SMTP.WRITE_RAW_DATA(
          vHANDLE,
          UTL_RAW.CONCAT(
            UTL_RAW.CAST_TO_RAW(CRLF)
            )
          );
      END LOOP;
    
      -- MIME終端
      UTL_SMTP.WRITE_RAW_DATA(
        vHANDLE,
        UTL_RAW.CONCAT(
          UTL_RAW.CAST_TO_RAW('--' || SEPARATOR || '--'),
          UTL_RAW.CAST_TO_RAW(CRLF)
          )
        );
    
      QUIT_MAIL(p_abort => false);
  
    EXCEPTION WHEN OTHERS THEN
      QUIT_MAIL(p_abort => true);
      RAISE_APPLICATION_ERROR(-20120, 'メール送信に失敗しました。' || SQLERRM, TRUE);
    END;
  END;
END MAILHELPER;
/

補足すると、このメール送信ヘルパでは、受信者は複数指定可能としている。その場合はMAIL_ADDRESS_LIST型の索引付き表に順番に設定しておく。

また、添付ファイルあり・なしで処理の内容が異なり、添付ファイルなしの場合は単純なTEXT/PLAINで送信している。

添付ファイルありの場合はContent-TypeをMIMEにして、boundaryにランダムな約50桁ぐらいの区切り文字を毎回作成し、
これを本文と各添付ファイルの開始マーカー、およびメール本文終了のマーカーとして使用している。

送信に失敗した場合、ORA-20120のユーザ定義例外が返される。

使用例(テストコード) 添付ファイルなしケース

添付ファイルなしのテスト。

送信先は二人。

メールアドレスには日本語による表記もつけている。

DECLARE
  P_SUBJECT NVARCHAR2(200);
  P_MESSAGE NVARCHAR2(200);
  
  P_MAIL_FROM MAILHELPER.MAIL_ADDRESS;
  P_MAIL_TO MAILHELPER.MAIL_ADDRESS;
  P_MAIL_TO_LIST MAILHELPER.MAIL_ADDRESS_LIST;
BEGIN
  P_MAIL_FROM.DESCRIPTION := '"開発用メール送信"<テスト担当>';
  P_MAIL_FROM.EMAIL := 'user1@localhost';

  P_MAIL_TO.DESCRIPTION := '開発用<1a>';
  P_MAIL_TO.EMAIL := 'user1@localhost';
  P_MAIL_TO_LIST(1) := P_MAIL_TO;

  P_MAIL_TO.DESCRIPTION := '開発用,メール送信サーバ,テストユーザ2';
  P_MAIL_TO.EMAIL := 'user2@localhost';
  P_MAIL_TO_LIST(2) := P_MAIL_TO;
  
  P_SUBJECT := 'テストメール123';
  P_MESSAGE := 'これは添付なしメールテストです。';

  MAILHELPER.SENDMAIL(
    P_FROM => P_MAIL_FROM,
    P_TO_LIST => P_MAIL_TO_LIST,
    P_SUBJECT => P_SUBJECT,
    P_MESSAGE => P_MESSAGE
  );
END;

受信結果は、こんな感じ。

使用例(テストコード) 添付ファイルありケース

本文と200kb超の添付ファイル10個を送信するテスト。

送信先は一人。

DECLARE
  P_FROM MAILHELPER.MAIL_ADDRESS;
  P_TO MAILHELPER.MAIL_ADDRESS;
  P_TO_LIST MAILHELPER.MAIL_ADDRESS_LIST;
  P_SUBJECT NVARCHAR2(4000);
  P_MESSAGE NVARCHAR2(4000);
  P_FILES MAILHELPER.ATTACHED_FILES;
  mes varchar2(4096);
  rd raw(4096);

BEGIN
  P_FROM.EMAIL := 'user2@localhost';
  P_TO.EMAIL := 'user1@localhost';
  P_TO_LIST(1) := P_TO;

  P_SUBJECT := 'これはとてもながいとてもながいとてもながいとてもながいとてもながい件名';
  FOR x IN 1..10 LOOP
    P_MESSAGE := P_MESSAGE || 'これは添付ありテストメールです。' || chr(10);
  END LOOP;
  
  DECLARE
    P_FILE MAILHELPER.ATTACHED_FILE;
  BEGIN
    FOR cnt IN 1 .. 10
    LOOP
      -- 添付ファイルデータを作成するためのバッファとして一時BLOBを作成する.
      DBMS_LOB.CREATETEMPORARY(p_file.CONTENT, TRUE, DBMS_LOB.CALL);
    
      -- 添付用テストデータ作成
      FOR idx IN 1..10000
      LOOP
        mes := 'data-' || cnt || ': メッセージNo. ' || to_char(idx) || chr(10);
        rd := UTL_I18N.STRING_TO_RAW(mes, 'UTF8');
        DBMS_LOB.WRITEAPPEND(p_file.CONTENT, UTL_RAW.LENGTH(rd), rd);
      END LOOP;
      
      p_file.CONTENT_TYPE := 'text/plain; charset=UTF-8';
      p_file.FILE_NAME := '添付ファイル・ダミーデータ №' || cnt || '.txt';

      P_MESSAGE := '添付サイズ(' || cnt || ')=' || DBMS_LOB.GETLENGTH(p_file.CONTENT)
        || CHR(10) || P_MESSAGE;
      
      p_files(cnt) := p_file;
    END LOOP;
  END;

  MAILHELPER.SENDMAIL(
    P_FROM => P_FROM,
    P_TO_LIST => P_TO_LIST,
    P_SUBJECT => P_SUBJECT,
    P_MESSAGE => P_MESSAGE,
    P_FILES => P_FILES
  );

   -- 一時BLOBの解放
   FOR idx IN p_files.FIRST .. p_files.LAST
   LOOP
     DBMS_LOB.FREETEMPORARY(p_files(idx).CONTENT);
   END LOOP;
END;

受信結果は、こんな感じ。

結論

試した限りではOutlook2010、Thunderbirdともに、うまく受信してくれた。


今回、このテストはWindows XP/Windows 7上のOracle 11g Expressを使い、
メールサーバには同じマシン上に立てたJavaによるMTAである「Apache James 2.3.2」を使ってみた。
(間違えて外部に不要なメールが漏れないようローカル配信以外は拒否にしている。)


おそらく、このPL/SQLによるメール送信パッケージは、Oracle 11gR2 Enterprise & LinuxSendmail の組み合わせでも大丈夫ではないかな、とは思うが、そこは、まだ試していない。

とりあえず実用的なコードに近くなったと思う。

これをたたき台に実コードをもってゆくことができそうである。

*1:もっとも、昨今ではクライアントからの入力やデータベースへの格納文字コードとして普通にUNICODEが使われており、メール送信時だけ特定文字が使えないなどというのは、いろいろ面倒であるから、この際、UTF-8BASE64で行く、というのも案外、まともな選択肢であろうとは思う。ISO2022-JPを無理に使う理由は現在は無いであろう。

*2:この種の目的のためにUTF-7が存在するのだが、現在はすでにUNICODE規格から外れてしまった存在であるため、いまさらUTF-7を使う必要もないであろう。

*3:できあがったACLはsys/acls相対パス云々とマニュアルにあるが、定義した実体がどこにあるのかはちょっとわからなかった。

*4:個人的には、今回の調査で、これがもっとも有益で、目から鱗な収穫でした。

*5:同様に、JavaでいうところのStringBuilderやStringWriterのような自動拡張する文字列バッファが欲しければCLOBを一時LOBとして初期化すればよい。こうすれば4000バイトの制限もおかまいなく大きなサイズのテキストをPL/SQLで組み立ててゆくことができるようになる。

*6:BASE64は、Wikipediaによると64桁ないし76桁で改行コードを入れることになっているらしい。