【问题标题】:IMAP message gets UnicodeDecodeError 'utf-8' codec can't decodeIMAP 消息获取 UnicodeDecodeError 'utf-8' codec can't decode
【发布时间】:2020-04-17 18:53:38
【问题描述】:

经过 5 小时的尝试,是时候寻求帮助了。筛选了所有与此相关的 stackoverflow 问题,但找不到答案。

代码是 gmail 解析器 - 适用于大多数电子邮件,但某些电子邮件会导致 UnicodeDecodeError。问题是“raw_email.decode('utf-8')”,但改变它(见 cmets)会导致下面的不同问题。

# Source: https://stackoverflow.com/questions/7314942/python-imaplib-to-get-gmail-inbox-subjects-titles-and-sender-name

import datetime
import time
import email
import imaplib
import mailbox
from vars import *
import re                   # to remove links from str
import string


EMAIL_ACCOUNT = 'gmail_login'
PASSWORD = 'gmail_psswd'

mail = imaplib.IMAP4_SSL('imap.gmail.com')
mail.login(EMAIL_ACCOUNT, PASSWORD)
mail.list()
mail.select('inbox')
result, data = mail.uid('search', None, "ALL") # (ALL/UNSEEN)

id_list = data[0].split()
email_rev = reversed(id_list)             # Returns a type list.reverseiterator, which is not list
email_list = list(email_rev)
i = len(email_list)

todays_date = time.strftime("%m/%d/%Y")

for x in range(i):
    latest_email_uid = email_list[x]
    result, email_data = mail.uid('fetch', latest_email_uid, '(RFC822)')
    raw_email = email_data[0][1]                                 # Returns a byte
    raw_email_str = raw_email.decode('utf-8')                    # Returns a str
    #raw_email_str = base64.b64decode(raw_email_str1)      # Tried this but didn't work.
    #raw_email_str = raw_email.decode('utf-8', errors='ignore')  # Tried this but caused a TypeError down where var subject is created because something there is expecting a str or byte-like 
    email_message = email.message_from_string(raw_email_str)

    date_tuple = email.utils.parsedate_tz(email_message['Date'])           
    date_short = f'{date_tuple[1]}/{date_tuple[2]}/{date_tuple[0]}'

    # Header Details
    if date_short == '12/23/2019':
        #if date_tuple:
        #    local_date = datetime.datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
        #    local_message_date = "%s" %(str(local_date.strftime("%a, %d %b %Y %H:%M:%S")))
        email_from = str(email.header.make_header(email.header.decode_header(email_message['From'])))
        subject = str(email.header.make_header(email.header.decode_header(email_message['Subject'])))
        #print(subject)
        if email_from.find('restaurants@uber.com') != -1:
            print('yay')

        # Body details
        if email_from.find('restaurants@uber.com') != -1 and subject.find('Payment Summary') != -1:
            for part in email_message.walk():
                if part.get_content_type() == "text/plain":
                    body = part.get_payload(decode=True)
                    body = body.decode("utf-8")             # Convert byte to str
                    body = body.replace("\r\n", " ")
                    text = re.sub(r'\w+:\/{2}[\d\w-]+(\.[\d\w-]+)*(?:(?:\/[^\s/]*))*', '', body)           # removes url links
                    text2 = text.translate(str.maketrans('', '', string.punctuation))
                    body_list = re.sub("[^\w]", " ",  text2).split()

                    print(body_list)
                    print(date_short)

                else:
                    continue

【问题讨论】:

  • 为了让您的生活更轻松,您可能需要查看imapclient.readthedocs.io/en/2.1.0。这处理大多数低级别的东西,并且非常易于使用。除非您实现邮件和 imap RFC 的所有边缘情况(包括各种邮件消息部分的不同编码等),否则您上面的代码永远不会可靠。
  • 谢谢@jerch!惊人的资源,它的工作原理!但是,它没有显示如何提取电子邮件的正文,这是我要解析的内容。我在什么地方错过了吗?
  • 对,imapclient 停止帮助处理消息本身(原因很简单 - 大附件会影响解析 - 完全不需要)。要可靠地解析原始邮件消息,请参考 stdlib 模块,如 email.message 和 email.parser (docs.python.org/3/library/email.html)。遗憾的是,邮件消息可能很复杂(由于具有不同编码和 mimetype 的部分逻辑),您必须通过文档来涵盖这些方面。
  • 谢谢@Jerch,您能否详细说明一下代码的外观?我尝试了各种形式的 smtplib、imaplib、imapclient 和电子邮件库,但均未成功。我使用 imaplib 非常接近,但无法找出问题所在。任何指导都会有所帮助!
  • 尝试使用高级库:pypi.org/project/imap-tools All is already parsed.

标签: python python-3.x email utf-8 imap


【解决方案1】:

我有同样的问题经过大量研究后,我意识到我只需要使用 email 中的 message_from_bytes 函数,而不是使用 message_from_string

所以你的代码只需替换:

 raw_email_str = raw_email.decode('utf-8')        
 email_message = email.message_from_string(raw_email_str)

到

email_message = email.message_from_bytes(raw_email)

应该像魅力一样工作:)

【讨论】:

    【解决方案2】:

    使用“ISO 8859-1”而不是“utf-8”

    【讨论】:

      【解决方案3】:

      下面是一个示例,如何使用 Python 标准库中的 imapclient 和 email.* 模块检索和读取邮件部分:

      from imapclient import IMAPClient
      import email
      from email import policy
      
      
      def walk_parts(part, level=0):
          print(' ' * 4 * level + part.get_content_type())
          # do something with part content (applies encoding by default)
          # part.get_content()
          if part.is_multipart():
              for part in part.get_payload():
                  get_parts(part, level + 1)
      
      
      # context manager ensures the session is cleaned up
      with IMAPClient(host="your_mail_host") as client:
          client.login('user', 'password')
      
          # select some folder
          client.select_folder('INBOX')
      
          # do something with folder, e.g. search & grab unseen mails
          messages = client.search('UNSEEN')
          for uid, message_data in client.fetch(messages, 'RFC822').items():
              email_message = email.message_from_bytes(
                  message_data[b'RFC822'], policy=policy.default)
              print(uid, email_message.get('From'), email_message.get('Subject'))
      
          # alternatively search for specific mails
          msgs = client.search(['SUBJECT', 'some subject'])
      
          #
          # do something with a specific mail:
          #
      
          # fetch a single mail with UID 12345
          raw_mails = client.fetch([12345], 'RFC822')
      
          # parse the mail (very expensive for big mails with attachments!)
          mail = email.message_from_bytes(
              raw_mails[12345][b'RFC822'], policy=policy.default)
      
          # Now you have a python object representation of the mail and can dig
          # into it. Since a mail can be composed of several subparts we have
          # to walk the subparts.
      
          # walk all parts at once
          for part in mail.walk():
              # do something with that part
              print(part.get_content_type())
          # or recurse yourself into sub parts until you find the interesting part
          walk_parts(mail)
      

      请参阅email.message.EmailMessage 的文档。在那里,您可以找到读入邮件消息所需的所有位。

      【讨论】:

      • 对于发送者声明错误的内容传输编码的许多现实世界的消息,这仍然可能失败。从历史上看,许多客户端声明了"us-ascii",但随后还是发送了一些未声明的 8 位编码;这些天来,许多人可能声称"utf-8",但实际上使用了其他东西。
      • 没错,但情况总是如此 - 如果某事物声明为 XY 但为 Z,则您遇到了更大的问题(需要更多涉及的恢复策略,而且不能这么容易地制定蓝图)。
      猜你喜欢
      • 2013-11-11
      • 2022-01-13
      • 1970-01-01
      • 1970-01-01
      • 2020-05-30
      • 2021-09-23
      • 2018-05-21
      • 1970-01-01
      相关资源
      最近更新 更多