none
smtpclient の 添付ファイル送信で末尾の1バイトが勝手に付加されてしまう

    質問

  • smtpclientを使ってメール送信プログラムを作成しているのですが、特定サイズの添付ファイル(87041バイト、バイナリ)のものを添付すると、何故か末尾に x00が付加されてしまい、メーラー等から開こうとしても正しいファイルと認識できない事象が発生しています。

    >その後に頂いた情報で、末尾に付加されるのはx00固定ではなく、最後のバイトの文字がもう一つ付加されるという状態のようです。

        Sub Main()
            Dim filePath = "C:\PG\File87041.ext"                        ' 添付ファイル
            Dim host = "smtp.xxxx.com"                                  ' 送信メールサーバ
            Dim port = 587                                              ' 送信メールサーバのポート
            Dim user = "taro@xxxx.com"                                  ' メールアカウント
            Dim pass = "XXXXXXXX"                                       ' メールアカウントのパスワード
            Dim toAddr = "hanako@xxxx.com"                              ' メール送信先
            Dim toName = "送信先"                                       ' メール送信先名
    
            Dim encoding = Text.Encoding.GetEncoding("UTF-8")
            Using att = New Net.Mail.Attachment(filePath, Net.Mime.MediaTypeNames.Application.Octet)
                att.NameEncoding = encoding
    
                Using msg = New Net.Mail.MailMessage()
                    msg.SubjectEncoding = encoding
                    msg.BodyEncoding = encoding
                    msg.From = New Net.Mail.MailAddress(user  , "管理者", encoding)
                    msg.To.Add(New Net.Mail.MailAddress(toAddr, toName, encoding))
                    msg.Subject = "テスト"
                    msg.Body = "テストです"
                    msg.Attachments.Add(att)
    
                    Dim smtpc = New Net.Mail.SmtpClient()
                    smtpc.DeliveryMethod = Net.Mail.SmtpDeliveryMethod.Network
                    smtpc.Host = host
                    smtpc.Port = port
                    smtpc.Timeout = 10000
                    smtpc.Credentials = New Net.NetworkCredential(user, pass)
                    smtpc.EnableSsl = False
    
                    smtpc.Send(msg)
                End Using
            End Using
        End Sub

    このコードで 87041バイトのバイナリファイルを添付すると、メール上では87042バイト(末尾にx00が付加)のものが送付されます。

    どなたか解決方法をご存知の方はいらっしゃいませんでしょうか。


    2018年4月20日 0:59

回答

  • 内部的には internal な Base64Stream クラスを使うようですが、このクラスに不具合がありますね。
    https://referencesource.microsoft.com/#System/net/System/Net/mail/Base64Stream.cs,234

    MimePart.Send で 0x4400 = 17,408 バイトという内部バッファごとに Base64Stream.Write が呼ばれますが、Base64Stream.Write が 1 バイトだけで呼ばれた場合、234 行目が 0 を返してしまうため、「書き込んでない扱い」でもう一回ループで呼ばれてしまうようです。

    つまり、17,409(17,408+1) バイトといった節目 + 1 バイトで問題が生じる可能性があります。(条件修正)
    ただし、節目によっては「正常終了」「1バイト水増し」「BASE64 展開でエラー」とパターンが分かれるようです。

    現実的には SmtpClient を使わないかしかないでしょうね…。

    2018年4月23日 14:28
    モデレータ
  • 私の環境(Windows 10 x64 Ver 1607, .Net Framework 4.6.2)でも再現しました。

    テストに使用した添付ファイルは https://hack.jp/rand.bin にアップしています。ファイルを送信し、添付ファイルを保存すると、送信前は 87041 バイトだったファイルサイズが、送信後は 87042 バイトとなります。末尾に 0x77 が付加されていました。

    原因の特定まではできていませんが、下記コードのように CDO 経由で同添付ファイルをメールを送った場合などは現象が発生しないため、System.Net.Mail 側に何らかの不具合があると思いました。

    Dim filePath = "C:\PG\File87041.ext"                        ' 添付ファイル
    Dim host = "smtp.xxxx.com"                                  ' 送信メールサーバ
    Dim port = 587                                              ' 送信メールサーバのポート
    Dim user = "taro@xxxx.com"                                  ' メールアカウント
    Dim pass = "XXXXXXXX"                                       ' メールアカウントのパスワード
    Dim toAddr = "hanako@xxxx.com"                              ' メール送信先
    Dim toName = "送信先"                                       ' メール送信先名
    
    Dim oMsg = CreateObject("CDO.Message")
    oMsg.From = user ' 送信元
    oMsg.To = toAddr ' 送信先
    oMsg.Subject = "テスト" ' 件名
    oMsg.TextBody = "テストです" ' メール本文
    oMsg.TextBodyPart.Charset = "ISO-2022-JP" ' 本文の文字コード
    oMsg.AddAttachment(filePath) ' 添付ファイル
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2 ' 認証を行う場合は 2
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserver") = host ' smtp サーバー
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = port ' ポート
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = 1 ' 認証方法 (1:basic認証 2:NTLM認証)
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendusername") = user ' ユーザー名
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendpassword") = pass ' パスワード
    oMsg.Configuration.Fields.Update
    oMsg.Send


    現象が改善されるまでは、上記のように CDO 経由でメールを送信するのはどうでしょうか?

    2018年4月23日 5:36
  • 現実的には SmtpClient を使わないかしかないでしょうね…。

    原因も特定され、不具合で間違いなさそうですね…。
    使い勝手は異なりますが、個人的には MailKit 推しです。

    参考までに、「SmtpClient のバージョンによる実装揺れ」の情報も掲載しておきます。

    http://d.hatena.ne.jp/kaorun/20060420/1145516533

    http://surferonwww.info/BlogEngine/post/2011/10/30/SmtpClient-cannot-send-attachment-with-long-non-ascii-filename.aspx

    http://blog.shibayan.jp/entry/20130317/1363521503

    • 回答としてマーク AkihikoInoue 2018年4月25日 7:19
    2018年4月23日 15:14

すべての返信

  • .NET Framework のバージョンぐらいは書きませんか? 

    (SmtpClient は .NET のバージョンによる違いが結構あるようです)

    2018年4月20日 1:45
  • SmtpClient には幾つかのバグがあることが知られており、また .NET Framework のバージョンによっても実装が異なるため、バージョンごとに固有の対処コードが必要になることがあります。

    ゆえに自分の場合は、SmtpClient は扱いにくいということで、普段は MailKit/MimeKit を使っています。

    【MailKitが公式に.NETのSmtpClientを置き換えることを明らかにした。】
    https://www.infoq.com/jp/news/2017/04/MailKit-MimeKit-Official
    https://www.infoq.com/news/2017/04/MailKit-MimeKit-Official
     
    【System.Net.Mail.SmtpClient is marked as obsolete in docs, but not in the source code】
    https://github.com/dotnet/docs/issues/1876

    そのため私自身は SmtpClient を使い慣れておらず、固有の事象については詳しくは無いのですが:

    まずは送信時の問題なのか、受信時の問題なのかの切り分けが必要かと思います。
    今回の事象に当てはまるかはさておき、当方では、特定のドメイン宛てのメールのみが破損する事例も経験していますので。(その時は相手側サーバーの問題だった)

    たとえば受信したメールの未加工データ(ヘッダー部も含めた eml ファイル)があると、メールクライアント側でファイルが破損しているのか、送出したメールに問題があるのかの判断ができるかもしれません。(メールクライアント側で BASE64 のデコードに失敗しているなど)

    あるいは別の方法(たとえば上記の MailKit など)で送信テストを行い、結果を比較してみるとか。

    2018年4月20日 4:37
  • それは失礼しました。

    発生を発見したのは3.5ですが、4.6でも同様の現象が発生することまで確認しています。

    2018年4月21日 4:56
  • 返答ありがとうございます。

    あまり詳しくないのですが、受信先を変えても同様のことが発生しているのでおそらく送信側の問題と思われます。

    MailKitについては知らなかったのですが、確認させて頂きます。

    2018年4月21日 4:59
  • .NET 4.6 で、「さくらインターネット」の SMTP サーバー経由で gmail.com にメールを送信してみましたが、x00 や 0x00 が付与される現象は確認できませんでした。正常に 87041 バイトのファイルとして受信できています。

    添付したファイルは下記のものです。

    Dim bin87401 = Enumerable.Repeat(CByte(&HAA), 87401).ToArray()
    Dim filePath = "C:\PG\File87041.ext"
    File.WriteAllBytes(filePath, bin87401)

    ファイルの内容によって左右されるものだとすると、現象を再現可能なファイルを公開していただかないと、判断が難しいところだと思います。上記のファイルでも発生するのであれば、SMTP サーバーを変更してみるなどの追加確認が必要かもしれません。

    なお、提示頂いたソースコードで送信させたメールを確認したところ、当方環境では下記のメッセージになっていました。
    改行部はすべて vbCrLf になっています。(下記では、本題と関係の無いヘッダー部は省略しています。)

    MIME-Version: 1.0{改行}
    From: =?utf-8?Q?=E7=AE=A1=E7=90=86=E8=80=85?= <{user変数の内容}>{改行}
    To: =?utf-8?Q?=E9=80=81=E4=BF=A1=E5=85=88?= <{toAddr変数の内容}>{改行}
    Date: 22 Apr 2018 11:25:54 +0900{改行}
    Subject: =?utf-8?B?44OG44K544OI?={改行}
    Content-Type: multipart/mixed;{改行}
     boundary=--boundary_0_0c064504-23f0-4c04-9f34-e72d5073f687{改行}
    {改行}
    {改行}
    ----boundary_0_0c064504-23f0-4c04-9f34-e72d5073f687{改行}
    Content-Type: text/plain; charset=utf-8{改行}
    Content-Transfer-Encoding: base64{改行}
    {改行}
    44OG44K544OI44Gn44GZ{改行}
    ----boundary_0_0c064504-23f0-4c04-9f34-e72d5073f687{改行}
    Content-Type: application/octet-stream; name=File87041.ext{改行}
    Content-Transfer-Encoding: base64{改行}
    {"Content-Disp" & "osition: at" & "tachment" な文字列}{改行}
    {改行}
    qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq{改行}
    qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq{改行}
    {以下同じ行が延々と続く}
    qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq{改行}
    qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq{改行}
    qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo={改行}
    ----boundary_0_0c064504-23f0-4c04-9f34-e72d5073f687--{改行}
    {改行}
    {改行}

    また、上記の受信メッセージに含まれていた BASE64 文字列を復元してみましたところ、元のデータと同じものが現れることも確認できました。

    Dim s1 = "44OG44K544OI44Gn44GZ"
    Dim s2 = String.Join(vbCrLf, Enumerable.Repeat(StrDup(68, "q"c), 1713)) & _
       vbCrLf & "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=" & vbCrLf
    
    '[テストです]
    MsgBox("[" & Encoding.UTF8.GetString(Convert.FromBase64String(s1)) & "]")
    '87401
    MsgBox(Convert.FromBase64String(s2).Length)
    • 編集済み 魔界の仮面弁士MVP 2018年4月22日 6:04 本文に Content-Disposition という文字列が含まれると文章の一部が欠損するので表現変更
    2018年4月22日 5:57
  • ご回答、ご検証ありがとうございます。

    頂いた内容を確認させていただいたのですが、添付ファイルのバイト数が異なっているようです。(87041バイトで現象発生)

    頂いたソース内では87401バイトになっているようです。

    再現性はあると思いますので、よろしくお願いいたします。

    2018年4月23日 1:08
  • 私の環境(Windows 10 x64 Ver 1607, .Net Framework 4.6.2)でも再現しました。

    テストに使用した添付ファイルは https://hack.jp/rand.bin にアップしています。ファイルを送信し、添付ファイルを保存すると、送信前は 87041 バイトだったファイルサイズが、送信後は 87042 バイトとなります。末尾に 0x77 が付加されていました。

    原因の特定まではできていませんが、下記コードのように CDO 経由で同添付ファイルをメールを送った場合などは現象が発生しないため、System.Net.Mail 側に何らかの不具合があると思いました。

    Dim filePath = "C:\PG\File87041.ext"                        ' 添付ファイル
    Dim host = "smtp.xxxx.com"                                  ' 送信メールサーバ
    Dim port = 587                                              ' 送信メールサーバのポート
    Dim user = "taro@xxxx.com"                                  ' メールアカウント
    Dim pass = "XXXXXXXX"                                       ' メールアカウントのパスワード
    Dim toAddr = "hanako@xxxx.com"                              ' メール送信先
    Dim toName = "送信先"                                       ' メール送信先名
    
    Dim oMsg = CreateObject("CDO.Message")
    oMsg.From = user ' 送信元
    oMsg.To = toAddr ' 送信先
    oMsg.Subject = "テスト" ' 件名
    oMsg.TextBody = "テストです" ' メール本文
    oMsg.TextBodyPart.Charset = "ISO-2022-JP" ' 本文の文字コード
    oMsg.AddAttachment(filePath) ' 添付ファイル
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2 ' 認証を行う場合は 2
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserver") = host ' smtp サーバー
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = port ' ポート
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = 1 ' 認証方法 (1:basic認証 2:NTLM認証)
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendusername") = user ' ユーザー名
    oMsg.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendpassword") = pass ' パスワード
    oMsg.Configuration.Fields.Update
    oMsg.Send


    現象が改善されるまでは、上記のように CDO 経由でメールを送信するのはどうでしょうか?

    2018年4月23日 5:36
  • kenjinote さん、返答ありがとうございます。

    0x77が付加されたという新しい情報もありますます謎が深まりますが、CDOでの実装は比較的容易だと思いますので、そちらで検証してみようかと思います。

    ありがとうございます。

    ===============================
    0x77 の付加が気になったので、kenjinoteさんが検証したバイナリファイルと、私のファイルとを
    比較してみました。
    そこで気がついたのは、私が検証していたファイルの最後のバイトは 0x00であり、kenjinoteさんのは 0x77であることから、特定の文字が末尾に付加されるのではなく、最後の文字が1バイト増殖するというのが正しい認識のようです。
    以上補足です。
    ===============================

    2018年4月23日 9:49
  • 内部的には internal な Base64Stream クラスを使うようですが、このクラスに不具合がありますね。
    https://referencesource.microsoft.com/#System/net/System/Net/mail/Base64Stream.cs,234

    MimePart.Send で 0x4400 = 17,408 バイトという内部バッファごとに Base64Stream.Write が呼ばれますが、Base64Stream.Write が 1 バイトだけで呼ばれた場合、234 行目が 0 を返してしまうため、「書き込んでない扱い」でもう一回ループで呼ばれてしまうようです。

    つまり、17,409(17,408+1) バイトといった節目 + 1 バイトで問題が生じる可能性があります。(条件修正)
    ただし、節目によっては「正常終了」「1バイト水増し」「BASE64 展開でエラー」とパターンが分かれるようです。

    現実的には SmtpClient を使わないかしかないでしょうね…。

    2018年4月23日 14:28
    モデレータ
  • なるほど、サイズ指定をミスっていますね。大変失礼いたしました。

    再現したので追加検証してみたところ、下記の破損が見られました。

    Dim src As Byte() = Enumerable.Repeat(CByte(&HAA), 87041).ToArray()
    Dim base64 As String = Convert.ToBase64String(src, Base64FormattingOptions.None)
    '
    '上記 base64 は、下記 s1 と同一の文字列です
    Dim s1 As String = StrDup(116054, "q") & "o="
    'SmtpClient で添付すると下記になっています(実際は68文字ごとに CrLf が挿入されます)
    Dim s2 As String = StrDup(116056, "q")
    '
    'たしかに 1 バイト余計
    MsgBox(Convert.FromBase64String(s1).Length)  '87041
    MsgBox(Convert.FromBase64String(s2).Length)  '87042

    Base64 変換のルールに沿って算出してみても、s1 の方が正しいはずですね。

    なお、先に紹介した MailKit を使ってみた場合には、87041 バイトで正しく送出されました。

    Imports System.IO
    Imports System.Text
    Imports MailKit
    Imports MailKit.Net.Smtp
    Imports MailKit.Security
    Imports MimeKit
    Imports MimeKit.Text
    
    Module Module1
    
        Sub Main(args As String())
            Dim filePath = "C:\PG\File87041.ext"                        ' 添付ファイル
            Dim host = "smtp.xxxx.com"                                  ' 送信メールサーバ
            Dim port = 587                                              ' 送信メールサーバのポート
            Dim user = "taro@xxxx.com"                                  ' メールアカウント
            Dim pass = "XXXXXXXX"                                       ' メールアカウントのパスワード
            Dim toAddr = "hanako@xxxx.com"                              ' メール送信先
            Dim toName = "送信先"                                       ' メール送信先名
    
            Dim log As IProtocolLogger
            Select Case UCase(args.FirstOrDefault())
                Case "/LOG"
                    log = New ProtocolLogger("C:\PG\SMTP.log")
                Case Else
                    log = New ProtocolLogger(Console.OpenStandardOutput())
            End Select
    
            Dim enc = Encoding.GetEncoding("UTF-8")
            Using client As New SmtpClient(log)
                client.Connect(host, port, SecureSocketOptions.None)
                client.Authenticate(user, pass)
                client.Timeout = 10000
    
                Dim msg As New MimeMessage()
                msg.From.Add(New MailboxAddress(enc, "管理者", user))
                msg.To.Add(New MailboxAddress(enc, toName, toAddr))
                msg.Subject = "テスト(MailKit)"
    
                'ここでは
                'Content-Type: Text/plain; charset=utf-8
                'Content-Transfer-Encoding: base64
                'のために Mulipart クラスでメッセージを組み立てていますが
                'Content-Type: text/plain; charset=utf-8
                'Content-Transfer-Encoding: 8bit
                'でも構わない場合は、BodyBuilder クラスを使ったほうが多分楽です
                '
                'Dim builder As New MimeKit.BodyBuilder()
                'builder.TextBody = "テストです"
                'builder.Attachments.Add(filePath)
                'msg.Body = builder.ToMessageBody()
    
                Dim multi As New Multipart("mixed")
    
                Dim part As New TextPart(TextFormat.Plain)
                part.ContentTransferEncoding = ContentEncoding.Base64
                part.SetText(enc, "テストです")
                multi.Add(part)
    
                Dim attachment As New MimePart()
                attachment.Content = New MimeContent(File.OpenRead(filePath), ContentEncoding.Default)
                attachment.ContentDisposition = New ContentDisposition(ContentDisposition.Attachment)
                attachment.ContentTransferEncoding = ContentEncoding.Base64
                attachment.FileName = Path.GetFileName(filePath)
                multi.Add(attachment)
    
                msg.Body = multi
    
                client.Send(msg)
            End Using
        End Sub
    
    End Module



    • 編集済み 魔界の仮面弁士MVP 2018年4月24日 5:00 誤って MimeKit に MailKit の URL をリンクさせていたので訂正。
    2018年4月23日 14:46
  • 現実的には SmtpClient を使わないかしかないでしょうね…。

    原因も特定され、不具合で間違いなさそうですね…。
    使い勝手は異なりますが、個人的には MailKit 推しです。

    参考までに、「SmtpClient のバージョンによる実装揺れ」の情報も掲載しておきます。

    http://d.hatena.ne.jp/kaorun/20060420/1145516533

    http://surferonwww.info/BlogEngine/post/2011/10/30/SmtpClient-cannot-send-attachment-with-long-non-ascii-filename.aspx

    http://blog.shibayan.jp/entry/20130317/1363521503

    • 回答としてマーク AkihikoInoue 2018年4月25日 7:19
    2018年4月23日 15:14
  • みなさん、検証も含めありがとうございます。
    結局SmtpClientの不具合で間違いないようですね。

    今回はCDOで実装し変えることにしました。ありがとうございます。

    2018年4月25日 4:24
  • AkihikoInoue さん、こんにちは。フォーラム オペレーターの立花です。
    MSDN フォーラムへご投稿くださいましてありがとうございます。

    皆様からの情報はお役に立ちましたでしょうか。
    もし参考となる投稿がございましたら、その投稿に [回答としてマーク]
    を設定くださいませ。
    後から同じ問題で参照した方が情報を素早く見つけられるようになります。

    お手数ではございますが、ご協力の程、どうぞよろしくお願いいたします。


    参考になった投稿には回答としてマークの設定にご協力ください
    MSDN/TechNet Community Support 立花楓

    2018年4月25日 4:44
    モデレータ