.NET 4.5でSystem.Net.Mailを使ってメール送信した時に、一部の環境で件名が部分的に文字化けする問題について

.NET Framework 4.5のSystem.Net.Mailを使ってメール送信していたら、件名の途中が何故か文字化けしてしまう問題が発生したので、その内容と対応した方法をメモします。

# 現象

↓のようなコードでメールを送信していたら、

```lang:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;

namespace SmtpTest {
    class Program {
        static void Main(string[] args) {

            using ( var smtpClient = new SmtpClient() )
            using ( var msg = new MailMessage() ) {
                msg.From = new System.Net.Mail.MailAddress("from@example.com");
                msg.To.Add(new System.Net.Mail.MailAddress("to@example.com"));

                msg.SubjectEncoding = Encoding.UTF8;
                msg.Subject = "System.Net.Mailでメールを送信してみるテストの件名";

                msg.BodyEncoding = Encoding.UTF8;
                msg.Body = @"test";

                smtpClient.Send(msg);
            }
        }
    }
}
```

このコードからメールを送信すると90%以上の宛先に対しては正しい件名で届くものの、一部のクライアントに対してだけメールの件名が「System.Net.Mailでメールを送??してみるテストの件名」のように、文章の途中の１文字が何故か２文字に分割された上で文字化けしてしまっている状態になってしまいました。

# 原因
原因を調べるため、.NET 4.5のReference Source ( http://referencesource.microsoft.com/ ) を探っていたら、System.Net.Mailの中で件名のエンコード処理が↓の手順で行われていて、このあたりが原因となっているようでした。
System.Net.Mail.Message#435あたり⇒http://referencesource.microsoft.com/#System/net/System/Net/mail/Message.cs,435

1. テキストをbyte列に変換する　http://referencesource.microsoft.com/#System/net/System/Net/mail/MimeBasePart.cs,47
2. byte列を3バイト単位でbase64文字列に変換しつつ、70文字毎に改行コードを入れていく http://referencesource.microsoft.com/#System/net/System/Net/mail/Base64Stream.cs,215
3. 変換した文字をSubjectヘッダにセットする

この手順だと、元々のテキストがアルファベットのみの場合であれば問題無いですが、日本語とか多バイト文字がテキストに含まれた場合、byte列に変換した後にバイト単位で改行文字が挿入されてしまう事になります。

そして、このような多バイト文字の1文字を構成するbyte列が複数行に分割されてしまった場合、一部のメール環境では正しくデコードすることができず文字化けが発生したものと推測されます。（改行を消してからデコードすれば○、行単位でデコードしてから改行を消すと文字化ける）

# 対策
原因はわかったのでこれについてなんとか対策を考えましたが、なかなかSystem.Net.Mailの中をいじるのはハードルが高かったため、そもそもMailMessageのSubjectプロパティにエンコード済みの文字列を入れるようにしました。
ただ、これについてもちょっと工夫が必要で、まずSubjectプロパティに対して値をセットした時の動きとして、

* 改行文字が含まれると実行時エラー
* ASCII文字以外が入るとプロパティへ値セットした時点でデコードし、送信時に上記の手順で再度エンコードし直す
* ASCII文字のみの場合は、空白文字の箇所で改行を入れる

といったものだったため、Subjectプロパティには、

* 多バイト文字が分割されない＆mimeエンコードされた文字列が最大73文字に収まるように、独自にmimeエンコードを施す
* 独自にmimeエンコードした文字をそれぞれ、空白文字で区切って1行テキストにする

※）RFC2822だと1行最大78文字ですが、少し余裕を見て最大73文字に収まるようにしてます。
http://www.puni.net/~mimori/rfc/rfc2822.txt

として作ったものをセットするようにしました。
この修正を行ったことで、送信されるメールのSubjectヘッダの内容が、

* 1行が最大73文字で収まっている
* 多バイト文字が行をまたがない

ようにすることができ、これによってこの文字化け問題は発生しなくなったようです。
この対策を行った状態のソースコードが↓のものです。参考まで。
（実際のmimeエンコード処理は、総武ソフトウェア推進所様のSmdn.Formats.Mimeライブラリを利用してます。 http://smdn.jp/works/libs/Smdn.Formats.Mime/ 便利なライブラリありがとうございます。）

```lang:Program2.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
using Smdn.Formats.Mime;

namespace SmtpTest {
    class Program2 {
        private const int MaxLength = 73;

        static void Main(string[] args) {
            using ( var smtpClient = new SmtpClient() )
            using ( var msg = new MailMessage() ) {
                msg.From = new System.Net.Mail.MailAddress("from@example.com");
                msg.To.Add(new System.Net.Mail.MailAddress("to@example.com"));

                msg.Subject = EncodeSubject("System.Net.Mailでメールを送信してみるテストの件名", Encoding.UTF8);

                msg.BodyEncoding = Encoding.UTF8;
                msg.Body = @"test";

                smtpClient.Send(msg);
            }
        }

        private static string EncodeSubject(string text, Encoding encoding) {
            var fromIndex = 0;
            var lines = new List<string>();

            for ( int toIndex = 2; toIndex < text.Length; toIndex++ ) {
                var maxLength = MaxLength;
                if ( lines.Count == 0 ) {
                    // 最初の1行だけは「Subject: 」が付く分最大文字数が少なくなる
                    maxLength -= "Subject: ".Length;
                }

                var current = text.Substring(fromIndex, toIndex - fromIndex);
                var encodedLength = MimeEncoding.Encode(current, MimeEncodingMethod.BEncoding, encoding).Length;

                if ( encodedLength > maxLength ) {
                    var element = text.Substring(fromIndex, toIndex - fromIndex - 1);
                    var encodedElement = MimeEncoding.Encode(element, MimeEncodingMethod.BEncoding, encoding);

                    lines.Add(encodedElement);
                    fromIndex = toIndex - 1;
                }
            }

            var lastElement = text.Substring(fromIndex);
            var encodedLastElement = MimeEncoding.Encode(lastElement, MimeEncodingMethod.BEncoding, encoding);
            lines.Add(encodedLastElement);

            return lines.Aggregate((a, b) => a + " " + b);
        }
    }
}
```