Отпавка эллектронного письма в кодироке 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)))
Уроки
- Чтобы отправить обычное письмо с ascii символами по SMTP можно использовать CL-SMTP:SEND-EMAIL
- Если сигнализируется ошибка USOCKET:UKNOWN-ERROR, скорее всего проблема с форматом аргументов
- А детально разобраться в этом можно, установив (setf cl-smtp::*debug* t)
- Чтобы использовать MIME, лучше подойдет CL-SMTP:WITH-SMTP-EMAIL в паре с CL-MIME:PRINT-MIME
- Для CL-MIME:TEXT-MIME's инитарга :CONTENT нужен вектор октетов, а не строка
- Для преобразования UTF-8 строки в вектор октетов можно задействовать ARNESI:STRING-TO-OCTETS