I (he said uncontroversially) agree with Jon Skeet -- I'm not sure why converting to quoted printable isn't already in C# somewhere. It's old as the hills, and email seems it'll keep it going for the foreseeable future.

I've seen some wacky ways of trying to do this, but it's really pretty easy. There are only two rules. You take a collection of bytes, here an array, and in the first rule, you only have two options. Either it's a "normal" byte -- which is between 33 and 127, inclusive, but exclusive of 61 (decimal) (since that's the =) -- or it's not. If it is, you add its ASCII char equivalent to the string. If it isn't, you add = plus the string representation of the byte as a two-digit hexadecimal number.

The only monkey wrench is the second rule: that no line can be more than 76 characters, though apparently you can cut any line you want shorter than that. So if the line you're on is 75 characters long already & you're adding a "normal" or if it's 73 characters or greater and you're adding a "non-normal", you need to add a = to the end of the line to let a parser know it's not really a new line of text yet. Then you add (always) a carriage return and line feed. That is, in another [more complicated] manner of speaking, package your data with a \r\n between every 75 characters (treating =$$ as a unit) except, if you're encoding data that is text, you don't escape your new line bytes, and the counting starts from zero after a textual new line.

Simple, right? Either/or check for byte values with a line length check.

public static string ToQuotedPrintable(this string self)
{
    byte[] bytes = System.Text.Encoding.UTF8.GetBytes(self);
    StringBuilder sbInner = new StringBuilder();
    StringBuilder sbOuter = new StringBuilder();

    foreach (byte byt in bytes)
    {
        int charLenQP = (byt >= 33 && byt <= 126 && byt != 61) ? 1 : 3;
        if (sbInner.Length + charLenQP > 75)
        {
            sbOuter.Append(sbInner + "=\r\n");
            sbInner = new StringBuilder();
        }

        if (1 == charLenQP)
        {
            sbInner.Append((char)byt);
        }
        else
        {
            sbInner.Append("=" + byt.ToString("X2"));
        }
    }
    sbOuter.Append(sbInner);
    return sbOuter.ToString();
}

I think that's right.

QUICK NOTE: I am encoding in UTF-8 every time right now. You could take that in as a parameter (maybe with a UTF-8 default), but the best case would be to send in a byte array from the start, which would let you decide your own encoding, but then we couldn't use it as a fancy string extension. Though perhaps it should be a byte array extension, eh? That might make more sense.

Labels: , , ,