2023年9月7日木曜日

C# メール送信プログラムの「SSPIへの呼び出しに失敗しました。内部例外を参照してください。」エラー対応

 数年前に作ったメール送信機能のあるプログラムを作り変えようとしたら、「SSPIへの呼び出しに失敗しました。内部例外を参照してください。」エラーが発生した。

以前作ったとき、.NETのSmtpClientがexplisit SSLのみの対応の、私が使っているjcomのサーバのimplicit SSLで発生が発生したため、そのとき見つけたAegisImplicitMailを利用していた。

今回のエラーはSmtpSocketConnection.Openメソッドの中の

    sslStream.AuthenticateAsClient(host)

で発生した。

検索してみたところ、参考になる記述を発見。

 

PowershellでSSL証明書情報取得時にTLSエラーにハマッた件    https://qiita.com/pizza_slice/items/00d00fd900bb3f0fd697

Windows Server 2016

Window 10 21H2 だったら正常にコマンドが通る。
恐らくこれもOS依存のTLSのバージョンの問題。

# 下記を参考にしてオーバーロード
# AuthenticateAsClient(String, X509CertificateCollection, SslProtocols, Boolean)
# Tls12:3072 TLS1.2セキュリティ プロトコルを指定します$stream.AuthenticateAsClient($commonName, $null, "3072", $false)


同じ理由かもしれないと、AuthenticateAsClientをこれに対応するメソッドに置き換えてみる。

    sslStream.AuthenticateAsClient(host, null, (SslProtocols)3072, false);

するとエラーが発生しなくなった。
(SslProtocols)3072 としたのは、プロジェクトの.NETのバージョンのせいかと思うが、SslProtocols 列挙型にTls12が含まれていなかったため、強引にそのInt値をcastしたからです。
列挙型の名前とint値の対照は次のページを参照してください。

SslProtocols 列挙型

追記

エラーが発生したのは.NET4.0でした。4.5(以上)に変更するとSslProtocols .Tls12が定義されており、上記のintからのcastは不要になります。

なお、このプロジェクトで使用しているAegisImplicitMailの.NETバージョンの関係で、4.5より新しいバージョンでは試していません。SslProtocols .Defaultの値にTls12が含まれていれば、AuthenticateAsClient(host)でもエラーが発生しなくなるでしょう。




2023年9月4日月曜日

VB Excelのシートを名前で探す方法 高速版

VBAでExcelのシートを名前で探す方法を調べると、たいていループで名前が一致するものを探す方法が紹介されいます。

たとえばこんなコードです。

    Sub Sample1(name As String)
      For i = 1 to Worksheets.Count
            if Worksheets(i).name = name Then
                Debug.Print Worksheets.Index
                Exit For
            End If
        Next
    End Sub

一回だけ実行する場合は十分高速で問題ないでしょう。しかしループの中でさらにこのループを実行しするため、場合によってはかなり時間がかかります。 かつ結果"シート名"と一致するものがない場合はシート数Xループ回数実行され、  無駄に時間を浪費します。

上記の例ではWorksheets(i)と引数にindexを使用しています。またiの範囲がCountの範囲内なので、Worksheet(i)がエラーを起こすこともありません。

Worksheetsは引数にシート名を使用することもできます。そこで次のようなコードを試してみます。

    Dim sheet As Worksheet
    Set sheet = Worksheets("シート名" )

"シート名"のWorksheetが存在する場合は問題ないのですが、存在しない場合は「インデックスが有効範囲にありません。」というエラーが発生します。Worksheet(i)でもiが有効範囲外であれば同じエラーが発生しますので、内部では同様のループ処理が行われていることが想像できます。

今度は、つぎのような方法を試してみます。

    Sub Sample2(name As String)
        On Error GoTo NotFound
        Debug.Print Worksheets(name).Index
    NotFound:
    End Sub

これでエラーが回避でき、コードもすっきりしているかと思います。面白いことに、計測してみると単純ループの55~60%程度の処理速度となります。75~80%のスピードアッ!!とも言えます。内部的にはループと同等のことを行っていると思いますが、コンパイル済みかどうかの差が大きいのでしょう。

ループによる実行回数が数千回になるような場合は、数単位での差になるのでバカにできません。

このひとつ前のブログ『InternetExplorer.getElementById 「オブジェクトが必要です」エラー対策』で使用した「On Error Resume Next」も試してみました。

    Sub Sample3name As String)
        Dim sheet As Worksheet
        On Error Resume Next
        Set sheet = Worksheets(name)
        If Not sheet Is Nothing Then
            Debug.Print Worksheets(name).Index
        End If
    End Sub

これは処理速度的にはSample2と同等でした。この例の場合ではSample2の方がコード的にもすっきりしていますが、使用する場面では使い道があるかもしれせん。

2023年8月18日金曜日

VB InternetExplorer.getElementById 「オブジェクトが必要です」エラー対策

VB InternetExplorer.getElementById 「オブジェクトが必要です」エラー対策

VBの InternetExplorer.getElementById は該当する要素がない場合に「オブジェクトが必要です」が発生する。これの扱いに困ったが、次の要領で対処できた。

まずは、次のようなコードでエラーが発生する。

    Dim elm As IHTMLElement
    Set elm = ie.document.getElementById("someID")

要素がない場合にgetElementById はNullを返す。
Nullを代入できるのはVariant型だけなので、IHTMLElementとかObjectで宣言した変数に代入しようとすると「オブジェクトが必要です」が発生する。

そこで、次のようなコードにしてみる。

    Dim elm As Variant
    Set elm = ie.document.getElementById("someID")

こんどは「型が一致しません」エラーが発生する。
これはSetではNullをObjectとして扱おうとするため発生する。

それでは、これではどうだろう。

    Dim elm As Variant, divs as IHTMLElementCollection
    elm = ie.document.getElementById("someID")
    if IsNull(elm) Then Exit Sub

    Set divs = ul.getElementsByTagName("div")

Nullのときの処理はうまくいくが、今度はelmがObjectとして設定されていないため、後続のコードで「オブジェクトが必要です」エラーが発生する。

そこで次のように変更し、エラー発生時に処理を継続してみた。

    On Error Resume Next
    Set elm = ie.document.getElementById("someID")
    If IsNull(elm) Then Exit Sub

    Set divs = someID.getElementsByTagName("div")

一応うまく処理できるようになったが、ちょっとおかしい。
IsNull(elm)がTrueにならない。この場合、elmがEmpty値になっている。
Setで代入するので、elmはObjectでなければならず、Nullにはならないということですね。
そのまま後続処理に進んでも上記のコードではエラーは発生しないが、具合が悪い場合もあるかもしれない。

そこで、次のようにするとIFの判別を次のようにすると有効になる。

    On Error Resume Next
    Dim elm As Object, divs as IHTMLElementCollection
    elm = ie.document.getElementById("someID")
    If elm Is Nothing Then Exit Sub 

On Error Resume Nextとした結果 elm がEmpty値になるため、elmをObjectで宣言している。

次の式でも判別できる。

    If IsEmpty(elm) Then Exit Sub

NothingはObjectとして空の状態、EmptyはVariant型として空の状態ということですね。Set式のあとなので実際にはObjectとして扱ってよいと思いますが、Dimで宣言した型に合わせるのがコード的には一貫性があると言えるでしょう。

実際にはNullになることはないが、次の方がより安全かもしれない。

    If IsNullOrEmpty(elm) Then Exit Sub

なお、

    On Error Resume Next

でエラーを無視するようにしたので、前後の処理の関係ではこのあとで再度On Errorを設定しなおす必要があるでしょう。