Пишу, по большей части, про историю, свою жизнь и немного про программирование.

Устранение потерь при кодировании почтовых аттачей в base64

Как известно, когда-то электронные письма предназначались исключительно для обмена текстом. Соотвественно и все протоколы тогда разрабатывались в расчёте на это. Когда появилась потребность передавать файлы, поступили очень просто — файлы специальным образом преобразовывались в текст и этот текст включался в тело письма. Конечно, такое преобразование не проходило даром — объём файла сильно увеличивался от этого пребразования, поскольку «текст» — это куда меньшее количество символов, чем бинарные данные, да и речь в те времена шла только об английском тексте. Со временем, это вошло в стандарт и почтовые программы научились самостоятельно вставлять в текст файлы, преобразуя их различными методами.

Прошло время, текст в электронных письмах давно уже не только английский, а стандарты кодирования текстовых файлов до сих пор используют только латинские буквы (плюс цифры и некоторые символы, такие как плюс, миную, слеш и так далее). Мне пришло в голову — а что если попробовать кодировать файлы всем полем допустимых символов. Сейчас, когда появились письма на UTF-8, допустимыми символами являются все, за исключением символов с кодами 0x00 (из-за того, что почти всё ПО написано на C/C++, где этот символ является признаком конца строки), 0x0A и 0x0D (это символы перевода строки, не хотелось бы, чтобы какой-нибудь умный клиент в бинарном файле добавил к 0x0D символ 0x0A — по стандарту в письмах перевод строки обозначается парой этих символов и, обычно, если встречается только один из них, почтовые клиенты или MTA добавляют и второй).

Из всех вариантов кодирования, а их три (Base64, UUencode и quoted-printable) для моих целей подошёл только quoted-printable. Идея его очень проста — все недопустимые символы (и «равно») преобразуются в последовательность из трёх символов: символ «равно» и шестнадцатиричный код символа. Поскольку «допустимых» символов традиционно очень мало, то метод не слишком экономичен.

Я же задался целью проверить, что же будет, если попробовать передать файл, где поле допустимых символов расширено до максимума. Сразу скажу — эксперимент удался. Можено эксперементально внедрять новый формат кодирования писем. Назовём его Bolk Quoted-Printable. Ладно, ладно, будем скромнее. 8bit Quoted-Printable.

Я написал небольшую программу на PHP, которая в качестве параметра использует имя файла, который нужно закодировать, а на выход выдаёт этот файл в формате понятном почтовому клиенту. Таким образом, если её запустить как «php -q 8qp.php somegif.gif > somename.eml» и открыть получившийся файл вашим почтовым клиентом, внутри вы должны увидеть свою картинку.

<?php
    // Example 1. 8qp.php
    // Written by Evgeny Stepanischev aka BOLK
    $bound = 'BOLKNOTE.RU';

    $mime = getimagesize($argv[1]);
    $mime = $mime['mime'];

    $str = file_get_contents($argv[1]);
    $len = filesize($argv[1]);

    echo <<<HEADER
Content-Type: multipart/mixed;
        boundary="----{$bound}"

------{$bound}
Content-Type: $mime; name="{$argv[1]}"
Content-Disposition: attachment; filename="{$argv[1]}"
Content-Transfer-Encoding: quoted-printable


HEADER;

    $np = array (0, 10, 13, 61);

    for ($i = 0; $i<$len; $i++)
    {
        $code = ord($str{$i});

        echo in_array($code, $np) ?
        '='.strtoupper(str_pad(dechex($code), 2, '0', STR_PAD_LEFT)):
        $str{$i};
    }

    echo "rn------{$bound}--";

Для эксперимента я выбрал наугад две картинки. JPEG и GIF. И вот что получилось в итоге. «TheBAT!» — это наименование моего почтового клиента.

Случайный GIF: исходный размер — 33590, мой quoted-printable — 36300, base64 encode — 46034, TheBAT! UUencode — 47052, TheBAT! Quoted-printable — 80576.

Случайный JPEG: исходный размер — 28144, мой quoted-printable — 29656, base64 encode — 38570, TheBAT! UUencode — 39429, TheBAT! Quoted-printable — 65663.

В случае QP-кодирования моим методом, размер файла, как видно по результатам двух экспериментов, увеличился совсем ненамного — 4-8%. Если для теста взять текстовый файл, то прирост будет едва заметен, если вообще будет, надо только переводы строки для текстовых файлов не кодировать, а пускать как есть.

Эксперименты на первом же попавшемся Sendmail показали, что, во-первых, типичный MX стремиться разбить строку по размеру своего буфера (у моей жертвы этот размер оказался равен приблизительно двум килобайтам) — но это легко устранимо, достаточно делать это за него и, что более неприятно, «умница» sendmail перекодирует письмо так как считает нужным — в традиционный quoted-printable.

Последняя беда поставила меня в тупик. Правда, ненадолго. Вспомнив, что большинство клиентов умеют открывать письма в формате EML я решил пойти на хитрость — прикрепить моё письмо как аттачмент другого, замаскировав моё письмо под обычный текст — Sendmail вряд ли проверяет аттачи глубже одного вхождения. А чтобы письмо максимально адекватно воспринималось почтовым клиентом, я дал ему disposition inline и убрал из него все служебные поля. Попробовал отправить — получилось. В TheBat видно только лишнее пустое поле — беда небольшая.

<?php
    // Example 2. 8qp_next.php
    // Written by Evgeny Stepanischev aka BOLK

    $bound  = '3D1E1C92aaBF00D';
    $bound2 = '3D1E1C92xxBF00D';

    $mime = getimagesize($argv[1]);
    $mime = $mime['mime'];

    $str = file_get_contents($argv[1]);
    $len = filesize($argv[1]);

    echo <<<HEADER
From: <mailbox@sample.com>
To: mailbox@sample.com
From: mailbox@sample.com
Content-Type: multipart/mixed;
 boundary="----------{$bound2}"

------------{$bound2}
Content-Type: message/rfc822; name="1.eml"; charset=utf-8
Content-Disposition: inline; name="1.eml"
Content-Transfer-Encoding: 8bit

Content-Transfer-Encoding: 8bit
Content-Type: multipart/mixed;
        boundary="----{$bound}"

------{$bound}
Content-Type: $mime; name="{$argv[1]}"; charset=utf-8
Content-Disposition: attachment; filename="{$argv[1]}"
Content-Transfer-Encoding: quoted-printable


HEADER;

    $np = array (0, 10, 13, 61);

    for ($i = 0; $i<$len; $i++)
    {
        $code = ord($str{$i});

        echo in_array($code, $np) ?
        '='.strtoupper(str_pad(dechex($code), 2, '0', STR_PAD_LEFT)):
        $str{$i};

        if ($i && ($i % 512) == 0) echo "=rn";
    }

    echo "rn------{$bound}--",
    "rn------------{$bound2}--";

Полный размер письма после такой двойной «упаковки» увеличивается незначительно. Кстати говоря, Opera 7.0 с таким письмом не справилась. Возможно, в силу старости — текущая версия этого пакета — 7.61b. Для интереса я отправил письмо на мои ящики на серверах Mail.ru и Gmail.com. Оба сервера вполне достойно справились с задачей, единственно, что Gmail не принимал письмо, пока я не указал charset как UTF-8.