[Заметка] Как считать емейл по imap c mail.ru в Python? Или что такое quoted printable encoding?

От автора: Я продолжаю писать заметки о насущных проблемах и я очень надеюсь, что буду не одинок в этом деле и
Вы тоже поделитесь какими-то небольшими решениями, которые помогли вам в автоматизации.

Предыдущая заметка была о RobotFramework, а в этот раз я хотел бы поделиться небольшим разъяснением, как же все таки работать с mail.ru.

Если Вы хоть когда-то читали сообщения по imap протоколу, но наверное знаете, что ничего сложного в этом нет. Вот например python код, который читает subject непросмотренных емейлов:

from imaplib import IMAP4, IMAP4_SSL

server = IMAP4_SSL('imap.gmail.com')
server.login(
    'email.productivity.conf@gmail.com',
    'happy@conf')

server.select()
result, ids = server.search(
    None, '(UNSEEN')
print "New emails with your email in TO is %d" % len(ids[0].split())
for id in ids[0].split():
    print "\t" + server.fetch(id,
        '(BODY.PEEK[HEADER.FIELDS (SUBJECT)])')[1][0][1].strip()

в результате получаем

New emails with your email in TO is 5
	Subject: [Lesson24] Your username and password
	Subject: Your Lesson24 order receipt from May 6, 2013
	Subject: Your Lesson24 order from May 6, 2013 is complete
	Subject: [Lesson24] Order Received
	Subject: Suspicious sign-in prevented

Проблема

Как бы все хорошо работает, но предположим у вас емейл не на gmail.com, а на yandex или mail.ru. И если мы применим этот же код, только к mail.ru:

from imaplib import IMAP4, IMAP4_SSL

server = IMAP4_SSL('imap.mail.ru')
server.login('your_loging@mail.ru', 'your_password_here')

server.select()
result, ids = server.search(None, '(UNSEEN)')
print "New emails with your email in TO is %d" % len(ids[0].split())
for id in ids[0].split():
    print "\t" + server.fetch(id,
        '(BODY.PEEK[HEADER.FIELDS (SUBJECT)])')[1][0][1].strip()

то получим совсем не то, что ожидаем:

New emails with your email in TO is 2
	Subject: =?utf-8?Q?=D0=9B=D1=83=D1=87=D1=88=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BC=D0=B5=D1=81=D1=8F=D1=86=D0=B0=20=D0=BE=D1=82=20Group=20Travel=21?=
	Subject: =?utf-8?Q?=D0=92=D1=81=D0=B5=20=D0=BF=D1=83=D1=82=D0=B5=D1=88=D0=B5=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D0=9C=D0=B0=D0=BD=D0=B4=D1=80=D1=83=D0=B9=20=D0=94=D0=B5=D1=88=D0=B5=D0=B2=D1=88=D0=B5=20=D0=BD=D0=B0=2005=2F10=2F2013?=

Решение

Почему такая странная кодировка? Потому что там есть русские символы. Но это ведь не unicode и не ascii, что же это? Это специальная quoted printable encoding, которая используется в MIME для передачи не ascii содержимого в емейле. Более подробно можно почитать на википедии Quoted-printable - Wikipedia

Ну и как теперь решать такую проблему? Просто, потому что в python есть встроенный модуль, который называется quopri. Все что нам нужно сделать - это декодировать полученную строку в нормальный божеский вид. Смотрим код:

import quopri
from imaplib import IMAP4, IMAP4_SSL

server = IMAP4_SSL('imap.mail.ru')
server.login('your_loging@mail.ru', 'your_password_here')

server.select()
result, ids = server.search(None, '(UNSEEN)')
print "New emails with your email in TO is %d" % len(ids[0].split())
for id in ids[0].split():
    subject = server.fetch(id,
        '(BODY.PEEK[HEADER.FIELDS (SUBJECT)])')[1][0][1].strip()
    print "\t" + quopri.decodestring(subject.encode('utf-8')).decode('utf-8')

получаем:

New emails with your email in TO is 2
	Subject: =?utf-8?Q?DOU-активность за 5 октября 2013?
	Subject: =?utf-8?Q?Все путешествия Мандруй Дешевше на 07/10/2013?

Дальше можно еще строки обрабатывать, но суть в том, чтобы превратить непонятные символы в понятные. Чего мы с вами и добились.

Дополнение

Забыл еще сказать, что кроме quoted printable encoding может быть и base64. Как это распознать?
Вот строка в quoted printable encoding:

=?utf-8?Q?=D0=92=D1=81=D0=B5=20=D0=BF=D1=83=D1=82=D0=B5=D1=88=D0=B5=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D0=9C=D0=B0=D0=BD=D0=B4=D1=80=D1=83=D0=B9=20=D0=94=D0=B5=D1=88=D0=B5=D0=B2=D1=88=D0=B5=20=D0=BD=D0=B0=2005=2F10=2F2013?=

и в base64:

Subject: =?UTF-8?B?0J/QvtC30LTRgNCw0LLQu9C10L3QuNGPINGBINC/0YDQsNC30LTQvdC40LrQsNC80Lgg0L3QtdC00LXQu9C4INGBIDcg0L/QviAxMyDQvtC60YLRj9Cx0YDRjywg0L3QvtCy0YvQtSDQv9GA0LjQutC+0LvRjNC90YvQtSDQvtGC0LrRgNGL0YLQutC4INC90LAg0LzQvtCx0LjQu9GM0L3Ri9C5IQ=?

Если хорошо присмотреться, то сразу заметите зарницу между ?utf-8?Q? и ?UTF-8?B?. Т.е. Q = quoted, B = base.

И как теперь декодировать base64? Правильно, с помощью специального модуля, смотрим код:

import base64
base64.b64decode('aHR0cDovLzR1ZnJlZS50ay9tZWRpYTcyMzY0Ni9mdWVuZi8wMzYubXAz')

на выходе получаем:

'http://4ufree.tk/media723646/fuenf/036.mp3'

От автора: Пишите свои заметки и комментируйте код. Хорошей вам автоматизации!

Зачем такие сложности? Почему нельзя использовать https://temp-mail.ru и их крайне простое API?

Ну я не спорю, что есть такие сервера, как http://mailinator.com/ который я сам часто использую.

Но все таки, бывает случаи что нужно подключиться и к mail.ru, тогда надо знать как это делать. Меня попросили показывать пример как можно подключиться к mail.ru, вот и был повод написать небольшую заметку.

@TIT Вот кстати у тебя и появился возможность написать небольшую заметку, как и когда использовать такие сервисы. Насколько я помню ты программишь на ruby, так что все должно быть предельно понятно и лаконично :smile:

Чтобы не плодить сущности, прокомментирую тут, если никто не против.
Когда мне надо было читать ссылки из почты (при регистрации чаще всего), то использовал такой свой класс.

# encoding: utf-8

class Mail
  require 'net/imap'
  require 'uri'

  def initialize(login, password, imap_server = "imap.yandex.ru")
    @login = login
    @password = password
    @imap_server = imap_server
  end
  
  def connect
    @imap = Net::IMAP.new(@imap_server)
    @imap.login(@login, @password)    
  end
  
  def disconnect
    @imap.logout
    @imap.disconnect
  end
  
  def get_link(from)
    @imap.select("INBOX")
    message_id = @imap.search(["FROM", from])
    link = URI.extract(@imap.fetch(message_id,'BODY[TEXT]')[0].attr['BODY[TEXT]'], ['http', 'https'])[0]
    @imap.store(1, "+FLAGS", [:Deleted])
    @imap.expunge
    return link
  end
end
2 лайка