首页 > 解决方案 > 如何使用 Python3 smtplib 实现 Gmail 风格的密件抄送电子邮件

问题描述

我正在尝试向应该被密件抄送以选择个人的用户发送电子邮件,也就是说,他们应该在收件人不知情的情况下获得一份副本。

我已经用 smtplib 实现了这一点,正如许多问题所指出的那样——你不应该在邮件头中包含密件抄送列表,而只能将它与传入的最终“待列表”合并sendmail——这很好。

但是,当我通过 web gmail 客户端密件抄送一封电子邮件时,被密件抄送的用户将他们的电子邮件放在该bcc字段下的标题中。我不明白如何实现这一点 - 这一定意味着有 2 个标头:一个没有密件抄送条目发送到“收件人”地址,另一个带有密件抄送标头发送到“密件抄送”地址,对吧?

这可能与 python 中的 smtplib 吗?我写了一个电子邮件函数来处理这个问题,但它最终在from_addr发送框中发送了 2 条单独的消息。Gmail 似乎能够避免这种情况。我不再使用 Outlook,但我记得与 Gmail 中的功能相同。

def email_it_via_gmail(headers, text=None, html=None, password=None):
    """Send an email -- with text and HTML parts.

    @param headers {dict} A mapping with, at least: "To", "Subject" and
        "From", header values. "To", "Cc" and "Bcc" values must be *lists*,
        if given.
    @param text {str} The text email content.
    @param html {str} The HTML email content.
    @param password {str} Is the 'From' gmail user's password. If not given
        it will be prompted for via `getpass.getpass()`.

    Derived from http://code.activestate.com/recipes/473810/ and
    http://stackoverflow.com/questions/778202/smtplib-and-gmail-python-script-problems and
    https://code.activestate.com/recipes/576824/
    """
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText
    import re
    import smtplib
    import getpass

    if text is None and html is None:
        raise ValueError("neither `text` nor `html` content was given for "
                         "sending the email")
    if not ("To" in headers and "From" in headers and "Subject" in headers):
        raise ValueError("`headers` dict must include at least all of "
                         "'To', 'From' and 'Subject' keys")

    # Create the root message and fill in the from, to, and subject headers
    msg_root = MIMEMultipart('related')
    for name, value in headers.items():
        if name == 'Bcc':
            continue
        msg_root[name] = isinstance(value, list) and ', '.join(value) or value
    msg_root.preamble = 'This is a multi-part message in MIME format.'

    # Encapsulate the plain and HTML versions of the message body in an
    # 'alternative' part, so message agents can decide which they want
    # to display.
    msg_alternative = MIMEMultipart('alternative')
    msg_root.attach(msg_alternative)

    # Attach HTML and text alternatives.
    if text:
        msg_text = MIMEText(text.encode('utf-8'), _charset='utf-8')
        msg_alternative.attach(msg_text)
    if html:
        msg_text = MIMEText(html.encode('utf-8'), 'html', _charset='utf-8')
        msg_alternative.attach(msg_text)

    to_addrs = headers["To"] \
               + headers.get("Cc", [])
    from_addr = msg_root["From"]

    bcc_addr = headers.get("Bcc", [])

    # Get username and password.
    from_addr_pats = [
        re.compile(".*\((.+@.+)\)"),  # Joe (joe@example.com)
        re.compile(".*<(.+@.+)>"),  # Joe <joe@example.com>
    ]
    for pat in from_addr_pats:
        m = pat.match(from_addr)
        if m:
            username = m.group(1)
            break
    else:
        username = from_addr
    if not password:
        password = getpass.getpass("%s's password: " % username)

    smtp = smtplib.SMTP('smtp.gmail.com', 587)  # port 465 or 587
    smtp.ehlo()
    smtp.starttls()
    smtp.ehlo()
    smtp.login(username, password)
    smtp.sendmail(from_addr, to_addrs, msg_root.as_string())
    if bcc_addr:
        for addr in bcc_addr:
            msg_root['Bcc'] = addr
            smtp.sendmail(from_addr, [addr], msg_root.as_string())
    smtp.close()


headers = {'To': ['bob@awesome.com'],
          'From': 'reporter@awesome.com',
          'Subject': 'TEST: smtp emails',
          'Bcc': ['joe@secret-awesome.com']}
email_it_via_gmail(headers, text="Test email.")    # This code sends 2 emails. 

(这不是我的功能,源代码在文档字符串中,但我删除了 BCC 标头并创建了 BCC 循环。)

上面的目标是将 1 封电子邮件发送到(至少)2 个地址,其中一个是密件抄送的,并且在他们的副本中,标头包括他们在“密件抄送”标头中的地址。在上面的代码中,发送了 2 封电子邮件(根据我在 gmail 中发送的文件夹)。

编辑:我刚刚尝试了雷鸟,似乎在该客户端中密件抄送的密件抄送地址根本没有在标题中看到自己。那么这可能只是 Gmail 操纵他们发送的文件夹并合并这些电子邮件吗?

我正在查看该sendmail函数,它显示为:

        (code, resp) = self.mail(from_addr, esmtp_opts)
        ...
        for each in to_addrs:
            (code, resp) = self.rcpt(each, rcpt_options)
        ...
        (code, resp) = self.data(msg)

因此,据我所知,smtp 进程与每个接收服务器建立连接,然后发出一次数据传输 - 查看self.rcpt代码,您似乎无法创建唯一池,该命令非常简单。 ..

所以我倾向于我所要求的不能用 smtplib 来完成......但我认为它不能用 smtp 来完成。也许这只是谷歌“清理”发送的文件夹标题,而不是任何特殊的发送。实际上,我的方法是否与上面的实现相同,只是缺少“干净”步骤?

编辑2:

我想在其中一个网站上创建一个免费的临时邮件帐户,然后我密件抄送该电子邮件以检查标题,密件抄送就在那里。

这样可以确认 gmail 发送单独的标头,以防有疑问。

标签: pythonpython-3.xemailsmtpsmtplib

解决方案


推荐阅读