2020-07-22

PL/SQLから日本語(JIS)でメール送信する

ここでは、

にあるサンプルコード
DECLARE
c UTL_SMTP.CONNECTION;
PROCEDURE send_header(name IN VARCHAR2, header IN VARCHAR2) AS
BEGIN
UTL_SMTP.WRITE_DATA(c, name || ': ' || header || UTL_TCP.CRLF);
END;
BEGIN
c := UTL_SMTP.OPEN_CONNECTION('smtp-server.acme.com');
UTL_SMTP.HELO(c, 'foo.com');
UTL_SMTP.MAIL(c, 'sender@foo.com');
UTL_SMTP.RCPT(c, 'recipient@foo.com');
UTL_SMTP.OPEN_DATA(c);
send_header('From', '"Sender" <sender@foo.com>');
send_header('To', '"Recipient" <recipient@foo.com>');
send_header('Subject', 'Hello');
UTL_SMTP.WRITE_DATA(c, UTL_TCP.CRLF || 'Hello, world!');
UTL_SMTP.CLOSE_DATA(c);
UTL_SMTP.QUIT(c);
EXCEPTION
WHEN utl_smtp.transient_error OR utl_smtp.permanent_error THEN
BEGIN
UTL_SMTP.QUIT(c);
EXCEPTION
WHEN UTL_SMTP.TRANSIENT_ERROR OR UTL_SMTP.PERMANENT_ERROR THEN
NULL; -- When the SMTP server is down or unavailable, we don't have
-- a connection to the server. The QUIT call raises an
-- exception that we can ignore.
END;
raise_application_error(-20000,
'Failed to send mail due to the following error: ' || sqlerrm);
END;

を、日本語JISコード(ISO-2022-JP)で送れるように書き換えてみます。
ついでに、SMTP認証あり、ポート番号を587に変更し、プロシージャ化しています。

CREATE OR REPLACE PROCEDURE SEND_MAIL(
IN_FROM_NAME VARCHAR2,
IN_FROM_ADDRESS VARCHAR2,
IN_TO_ADDRESS VARCHAR2,
IN_SUBJECT VARCHAR2,
IN_MESSAGE VARCHAR2,
IN_AUTH_USER VARCHAR2,
IN_AUTH_PASSWORD VARCHAR2
) AS
c UTL_SMTP.CONNECTION;
host CONSTANT VARCHAR2(64) := 'foo.com';
port CONSTANT NUMBER := 587;
-- ヘッダー部送信
-- UTL_ENCODE.MIMEHEADER_ENCODE(buf, 'ISO2022-JP')を使うと=?iso-2022-jp?B?が=?ISO2022-JP?B?になるため環境により文字化けする。
-- Each line of characters MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF.(RFC 2822 2.1.1)
PROCEDURE SEND_HEADER (
IN_NAME IN VARCHAR2
,IN_HEADER IN VARCHAR2
,IN_ADDRESS IN VARCHAR2 := NULL
) AS
BEGIN
UTL_SMTP.WRITE_DATA(c, IN_NAME || ': ');
UTL_SMTP.WRITE_DATA(c, '=?iso-2022-jp?B?');
UTL_SMTP.WRITE_RAW_DATA(c, UTL_ENCODE.BASE64_ENCODE(UTL_RAW.CAST_TO_RAW(CONVERT(IN_HEADER, 'ISO2022-JP'))));
UTL_SMTP.WRITE_DATA(c, '?=');
IF IN_ADDRESS IS NOT NULL THEN
UTL_SMTP.WRITE_DATA(c, '<' || IN_ADDRESS || '>');
END IF;
UTL_SMTP.WRITE_DATA(c, UTL_TCP.CRLF);
END;
BEGIN
c := UTL_SMTP.OPEN_CONNECTION(host, port);
UTL_SMTP.EHLO(c, host);
-- SMTP認証(PLAIN)
-- 11.2.0.1以前
--UTL_SMTP.COMMAND(c, 'AUTH', 'PLAIN ' || UTL_ENCODE.TEXT_ENCODE(CHR(0)|| IN_AUTH_USER || CHR(0)|| IN_AUTH_PASSWORD, NULL, UTL_ENCODE.BASE64));
-- 11.2.0.2以上
UTL_SMTP.AUTH(c => c, username => IN_AUTH_USER, password => IN_AUTH_PASSWORD, schemes => UTL_SMTP.all_schemes);
UTL_SMTP.MAIL(c, IN_FROM_ADDRESS);
UTL_SMTP.RCPT(c, IN_TO_ADDRESS);
UTL_SMTP.OPEN_DATA(c);
--ヘッダ
SEND_HEADER('From', IN_FROM_NAME, IN_FROM_ADDRESS);
UTL_SMTP.WRITE_DATA(c, 'To: ' || IN_TO_ADDRESS || UTL_TCP.CRLF);
SEND_HEADER('Subject', IN_SUBJECT);
UTL_SMTP.WRITE_DATA(c, 'MIME-Version: 1.0' || UTL_TCP.CRLF);
UTL_SMTP.WRITE_DATA(c, 'Content-Type: text/plain; charset=iso-2022-jp' || UTL_TCP.CRLF);
UTL_SMTP.WRITE_DATA(c, 'Content-Transfer-Encoding: 7bit' || UTL_TCP.CRLF);
UTL_SMTP.WRITE_DATA(c, UTL_TCP.CRLF);
-- 本文
UTL_SMTP.WRITE_RAW_DATA(c, UTL_RAW.CAST_TO_RAW(CONVERT(IN_MESSAGE, 'ISO2022-JP')));
UTL_SMTP.CLOSE_DATA(c);
UTL_SMTP.QUIT(c);
EXCEPTION
WHEN UTL_SMTP.transient_error OR UTL_SMTP.permanent_error THEN
BEGIN
UTL_SMTP.QUIT(c);
EXCEPTION
WHEN UTL_SMTP.TRANSIENT_ERROR OR UTL_SMTP.PERMANENT_ERROR THEN
NULL; -- When the SMTP server is down or unavailable, we don't have
-- a connection to the server. The QUIT call will raise an
-- exception that we can ignore.
END;
raise_application_error(-20000,
'Failed to send mail due to the following error: ' || sqlerrm);
END;
/


  • ヘッダのエンコードにはUTL_ENCODE.MIMEHEADER_ENCODEを使いたいところですが、キャラクタ・セットが iso-2022-jp ではなく ISO2022-JP と出力されてしまい、環境によって文字化けするため、使わないようにしています。
  • RFC2822では1行の文字数について、CRLFを除く998未満(MUST)、78文字未満(SHOULD)と規定されており、UTL_ENCODE.MIMEHEADER_ENCODEを使えば1行当たり78文字未満に分割(folding)してくれるのですが、上記のコードでは考慮していませんのでご注意ください。

2020-06-24

VBA 丸めのまとめ

VBA(Visual Basic for Application)における数値の丸めについてまとめておきます。

偶数丸め(JIS丸め、銀行丸め)

VBA標準のRound関数は偶数丸め(JIS丸め、銀行丸め)なのでそのまま使えるのですが、整数部での丸め(小数部桁数に負の値を指定する)ができません。以下の関数を作り、拡張します。

' 偶数丸め(整数部丸め対応)
Public Function RoundHalfEven(ByVal number As Double, Optional ByVal digitsAfterDecimal As Long = 0) As Double
If digitsAfterDecimal < 0 Then
Dim shift As Double
shift = 10 ^ digitsAfterDecimal
RoundHalfEven = Round(number * shift) / shift
Else
RoundHalfEven = Round(number, digitsAfterDecimal)
End If
End Function

四捨五入

Excel VBAの場合、ワークシート関数のRoundを使えばよいのですが、Access や VBScript では使えないので自作しておきます。

' 四捨五入
Public Function RoundHalfUp(ByVal number As Double, Optional ByVal digitsAfterDecimal As Long = 0) As Double
Dim shift As Long
shift = 10 ^ digitsAfterDecimal
'浮動小数点誤差を避けるため十進型に変換する
RoundHalfUp = Fix(CDec(number) * shift + 0.5 * sgn(number)) / shift
End Function

Decimal型はVBAでは宣言できないため、CDec関数で変換する必要があります。Currency 型は小数部4桁に制限されるため使用しません。

なお、Decimal型との演算はDecimal型に変換されて行われます。

? 1.2-1.1
9.99999999999999E-02
? CDec(0)+1.2-1.1
0.1

有効数字に丸め

有効数字に丸める場合です。以下は偶数丸めの場合で上述のRoundHalfEven関数を使用しています。四捨五入で丸める場合は、上述のRoundHalfUp関数に置き換えてください。
桁数の初期値は3桁にしてあります。

'有効数字に丸め(初期値3桁)
Public Function RoundSignificantFigures(ByVal number As Double, Optional ByVal digits As Long = 3) As Double
If number = 0 Then
RoundSignificantFigures = 0
Else
' 偶数丸め
RoundSignificantFigures = RoundHalfEven(number, digits - 1 - Int(Log(number) / Log(10)))
End If
End Function

有効数字取得の考え方は、Oracleで有効数字を取得する。を参考にしてください。

床関数(Floor)

VBAのInt関数は床関数(Floor Function)と同じですので、そのまま使えます。床関数は実数x に対して x 以下の最大の整数と定義されます(Wikipedia
)が、Excelワークシート関数と同様に第2引数に基準値がとれるように拡張します。

' 床関数
Public Function Floor(ByVal number As Double, Optional ByVal significance As Double = 1) As Double
If significance = 0 Or (number > 0 And significance < 0) Then
Err.Raise 5 'プロシージャの呼び出し、または引数が不正です。
End If
Floor = Int(CDec(number) / significance) * significance
End Function

第1引数が正で第2引数が負の場合、正しい結果を返さないので、Excelのワークシート関数 Floorと態度を合わせてエラーにしています。

天井関数(Ceiling, Ceil)

天井関数(Ceiling, Ceil)は、数直線をイメージすると床関数を左右反転したものです。したがって、数値を正負反転して床関数で処理し、元に戻せばよいことになります。

' 天井関数
Public Function Ceiling(ByVal number As Double, Optional ByVal significance As Double = 1) As Double
If significance = 0 Or (number > 0 And significance < 0) Then
Err.Raise 5 'プロシージャの呼び出し、または引数が不正です。
End If
Ceiling = -Int(-CDec(number) / significance) * significance
End Function