Регистрация | Войти
Lisp — программируемый язык программирования

Отпавка эллектронного письма в кодироке utf-8

Автор: Всеволод Дёмкин

Источник: http://lisp-univ-etc.blogspot.com/2009/02/sendind-smtp-mail-with-utf-8-characters.html

В этой заметке исследуется тема отправки закодированного в base64 письма (с русским оригиналом) по SMTP. Из Common Lisp с помощью открытых библиотек (не Franz инфраструктуры). Как оказалось, для этого нужно задействовать целых 4 библотеки, не сильно документированные, поэтому я решил записать, как с ними расправиться...

CL-SMTP

Изначально, кроме HELO и EHLO я мало что знал об SMTP протоколе. Свое изучение его я начал с того, что попробовал решить задачу в лоб с помощью функции SEND-EMAIL [1]. Функция не документированна (по идее, должны быть все итак понятно :) и не сигнализирует собственных ошибок, а просто прокидывает ошибки от USOCKET'а, на который она опирается. Однако, есть все же один способ отладки в CL-SMTP с помощью

(setf cl-smtp::*debug* t)

[2]
Собственно, почему мне понадобилась отладка, так это из-за того, что вот это работало:

(cl-smtp:send-email "localhost"
                    "noreply@our.domain.net"
                    "test@gmail.com"
                    "subject"
                    "test"
)

а вот это выдавало USOCKET:UKNOWN-ERROR [2]:

(cl-smtp:send-email "localhost"
                    "noreply@our.domain.net"
                    "test@gmail.com"
                    "subject"
                    "тест"
)

(Пояснение: отправляем пичьмо с темой "subject" от noreply@our.domain.net на test@gmail.com через localhost)

Оказалось, что SMTP-сервер не принимает не ascii символы в теле письма, потому что кодировкой по умолчанию является 7bit.

CL-MIME

В общем, пришлось спросить Google, который ответил вот этой статьей от Ханса Хюбнера, в которой он объясняет свои доработки к CL-SMTP, которые сейчас уже интегрированны в библиотеку. Для их правильного применения нужно разобраться с тем, как работает MIME. В примере Ханс использует multipart/mixed Content-type для отсылки писем с вложенями. Но для нашей простой задачи отправки письма с русским текстом в кодировке UTF-8 это не обязательно. Сгодится и text/plain, но для того, чтобы SMTP-сервера принимали не-ascii символы, нужно использовать другой Content-encoding: base64. А управляется весь этот механизм с помощью библиотеки CL-MIME. В данном случае она вполне самодокументированна, поэтому отсутствие полноценной документации не влияет на возможности ее использования.

Для формирования данных в нужном нам кодировании используется функция PRINT-MIME [4], которая берет на вход CLOS-объект MIME с заданными полями. Единственная проблема с ней в том, что результирующие данные содержат как то, что должно пойти в тело письма, так и некоторые SMTP-заголовки. Поэтому ее вывод не может использоваться как аргумент для SEND-EMAIL: заголовки попадут в тело пиьсма (будут разделены новой строкой) и не будут учитываться на серверах. Для этого и других случаев, когда нужен больший контроль над процессом SMTP взаимодействия, Ханс ввел высокоуровневый макрос WITH-SMTP-MAIL [5]. У него есть небольшое отлтичие от SEND-EMAIL: вместо одного получателя (в виде строки e-mail адреса), он принимает список получателей.

CL-BASE64 и ARNESI

Однако, что мне доставило больше всего хлопот — это неочевидная обработка инитарга :CONTENT [6] для MIME-объектов. Если задан инитарг :ENCODING, например, :BASE64, :CONTENT будет поддан соответствуемуму кодированию (которое выполняется для данной кодировки с помощью библиотеки CL-BASE64). Интересно, что это даст неверные данные для UTF-8 строк. Правильный формат этого аргумента не строка, а octet array.

Т.е. нужна функция, чтобы привести строку к нему. Для этого можно задействовать библиотеку ARNESI, которая является собранием разнообразных утилит. Она как раз предоставляет такую функциональность через функцию STRING-TO-OCTETS [7].

Стоит также заметить, что если не-ascii символы будут присутствовать в теле письма (с правильным Contnet-encoding, т.е. не 7bit), они могут быть отправлены и так. Однако, конечно, наиболее работоспособный вариант для любой ситуации — использовать данные, закодированные в base64.

Результат

Короче говоря, в итоге у нас получилось нечто в этом духе:

(defun send-email (text &rest reciepients)
  "Generic send SMTP mail with some TEXT to RECIEPIENTS"
  (cl-smtp:with-smtp-mail (out "localhost" "noreply@fin-ack.com" reciepients)
    (cl-mime:print-mime out
                        (make-instance 'cl-mime:text-mime
                                       :encoding :base64
                                       :charset "UTF-8"
                                       :content (arnesi:string-to-octets text :utf-8)
)

                        t t
)
)
)

Уроки

  1. Чтобы отправить обычное письмо с ascii символами по SMTP можно использовать CL-SMTP:SEND-EMAIL
  2. Если сигнализируется ошибка USOCKET:UKNOWN-ERROR, скорее всего проблема с форматом аргументов
  3. А детально разобраться в этом можно, установив (setf cl-smtp::*debug* t)
  4. Чтобы использовать MIME, лучше подойдет CL-SMTP:WITH-SMTP-EMAIL в паре с CL-MIME:PRINT-MIME
  5. Для CL-MIME:TEXT-MIME's инитарга :CONTENT нужен вектор октетов, а не строка
  6. Для преобразования UTF-8 строки в вектор октетов можно задействовать ARNESI:STRING-TO-OCTETS
@2009-2013 lisper.ru