从(Python)代码实际发送邮件的正确方法是什么?

Fil*_* W. 15 python email networking gmail smtp

免责声明:由于这个问题的广泛性(见下文;-),我对标题犹豫不决,其他选项包括:

  • 如何使用Python代码从localhost发送邮件?
  • 如何在不使用外部SMTP服务器的情况下从Python代码发送电子邮件?
  • 是否可以使用localhost和Python直接向其目的地发送电子邮件?

首先,一点上下文:
为了学习,我正在构建一个具有用户注册功能的网站.这个想法是,注册后用户将收到一封带有激活链接的电子邮件.我想编写并发送来自Python代码的电子邮件,这是我想要求澄清的部分.


我的理解,在我开始之前(显然天真=)可以这样说明(假设有一个合法的电子邮件地址user@example.com):

我天真的理解

搜索的例子后,我碰上#1(的一些问题与解答1,2,3,4).从这些我已经提炼出以下片段,撰写并发送来自Python代码的电子邮件:

import smtplib

from email.message import EmailMessage

message = EmailMessage()
message.set_content('Message content here')
message['Subject'] = 'Your subject here'
message['From'] = 'me@example.com'
message['To'] = 'user@example.com'

smtp_server = smtplib.SMTP('smtp.server.address:587')
smtp_server.send_message(message)
smtp_server.quit()
Run Code Online (Sandbox Code Playgroud)

接下来(明显的)问题是要传递给什么smtplib.SMTP()而不是'smtp.server.address:587'.从评论到这个答案,我发现本地SMTP服务器(仅用于测试目的)可以通过python3 -m smtpd -c DebuggingServer -n localhost:1025,然后smtp_server = smtplib.SMTP('smtp.server.address:587')可以更改为,smtp_server = smtplib.SMTP('localhost:1025')并且所有已发送的电子邮件将显示在控制台中(从python3 -m smtpd -c DebuggingServer -n localhost:1025执行命令的位置),足以进行测试 - 这不是我想要的(我的目标是 - 能够使用Python代码从本地机器向'真实世界'电子邮件地址发送邮件).

因此,下一步是设置一个本地SMTP服务器,能够向外部"真实世界"的电子邮件地址发送电子邮件(因为我想从Python代码中完成所有操作,因此服务器本身最好在Python也).我回忆起在一些杂志(2000年初)中读取垃圾邮件发送者使用本地服务器发送邮件(特定文章谈论的是Sambar,其开发已于2007年结束,而且不是用Python编写的:-)我认为那里应该是一些具有类似功能的现今解决方案.所以我开始搜索,我希望找到(在stackoverflow或其他地方)一个相当短的代码片段,它将做我想要的.我还没有找到这样的代码片段,但我遇到了一个标题为(Python)发送电子邮件而不使用邮件服务器(使用chilkat API)的片段,尽管我需要的(据说)只是在那里,在代码的注释中,第一行明确说明:

是否真的可以在不连接邮件服务器的情况下发送电子邮件?并不是的.

以下几行:

以下是声称不需要邮件服务器的其他组件内部发生的情况:组件使用预期收件人的电子邮件地址执行DNS MX查找,以查找该域的邮件服务器(即SMTP服务器).然后它连接到该服务器并发送电子邮件.您仍然连接到SMTP服务器 - 而不是您的服务器.

阅读这些,让我理解 - 我显然缺乏一些细节(我在上面的图片中反映过程).为了纠正这个问题,我已经在SMTP上阅读了整个RFC.


阅读完RFC后,对该过程的理解有所改善,可能如下图所示: 我的理解提高了


根据这种理解,我想澄清一些实际问题:

  1. 作为一般情况,我的" 改善理解 " 能否被认为是正确的?
  2. 确切地说,MS查找返回了哪些地址?

    • 使用host -t mx gmail.com命令(由此答案建议),我能够检索以下内容:

      gmail.com mail is handled by 10 alt1.gmail-smtp-in.l.google.com.
      gmail.com mail is handled by 20 alt2.gmail-smtp-in.l.google.com.
      gmail.com mail is handled by 40 alt4.gmail-smtp-in.l.google.com.
      gmail.com mail is handled by 30 alt3.gmail-smtp-in.l.google.com.
      gmail.com mail is handled by 5 gmail-smtp-in.l.google.com.
      
      Run Code Online (Sandbox Code Playgroud)
    • 但这些都不在被提及的官方文档(那些不在那里smtp-relay.gmail.com,smtp.gmail.com,aspmx.l.google.com)
  3. 是否始终需要身份验证才能将电子邮件传递给已建立的邮件服务(例如gmail)的SMTP服务器?

    • 我知道要使用,比如smtp.gmail.com邮件提交,你需要,无论收件人是否有@gmail地址(如文档中所述):

      您需要完整的Gmail或G Suite电子邮件地址进行身份验证.

    • 但是,如果将电子邮件user@gmail.com提交给不属于gmail的SMTP服务器,则会将其重定向到其中一个gmail服务器(直接或通过网关/中继).在这种情况下(我假设)电子邮件的发件人只需要在邮件提交上进行身份验证,那么之后gmail服务器将接受邮件而不进行身份验证?

      • 如果是,是什么阻止我"假装"成为这样的网关/中继并将电子邮件直接交给他们指定的SMTP?然后,编写"代理SMTP"也很容易,它只会通过MX查找搜索合适的服务器,并直接向其发送电子邮件.
  4. 关于gmail SMTP的文档,也提到了aspmx.l.google.com不需要身份验证的服务器,但是:

    邮件只能发送给Gmail或G Suite用户.

    话虽如此,我假设以下代码片段可以正常工作,用于向ExistingUser@gmail.com邮箱提交邮件:

    import smtplib
    
    from email.message import EmailMessage
    
    message = EmailMessage()
    message.set_content('Message test content')
    message['Subject'] = 'Test mail!'
    message['From'] = 'me@whatever.com'
    message['To'] = 'ExistingUser@gmail.com'
    
    smtp_server = smtplib.SMTP('aspmx.l.google.com:25')
    smtp_server.send_message(message)
    smtp_server.quit()
    
    Run Code Online (Sandbox Code Playgroud)

    运行时,上面的代码(ExistingUser@gmail.com由有效邮件替换)抛出OSError: [Errno 65] No route to host.我想在此确认的是,aspmx.l.google.com在代码中正确处理了通信.

小智 6

感谢这些答案,感谢我的其他问题:1 , 2 , 3,以及其他人的这两个问题(和答案):,两个\xe2\x80\x94 我想我现在已经准备好回答我的问题了已发布,我自己。

\n\n
\n\n

我将一一解答大家的疑问:

\n\n
    \n
  1. 是的,作为一般情况,电子邮件的发送可以这样描述:\n我的理解有所提高

  2. \n
  3. MX 查找返回接收发往指定域的电子邮件的服务器地址。

    \n\n
      \n
    • 至于“为什么smtp-relay.gmail.com, smtp.gmail.com,aspmx.l.google.com没有被host -t mx gmail.com命令返回?”。这个问题的另一个答案几乎涵盖了这一点。这里要掌握的要点是:\n\n
        \n
      • MX 查找返回的服务器负责接收该域的电子邮件(在本例中为 gmail)
      • \n
      • gmail 文档中列出的服务器用于邮件发送(即 gmail 用户想要发送给其他 gmail 用户或以其他方式发送的邮件被提交到这些服务器)
      • \n
    • \n
  4. \n
  5. 对于接收电子邮件的服务器(即 MX 查找返回的电子邮件),不需要身份验证。

    \n\n
      \n
    • 有几件事可以防止此类服务器被滥用:\n\n
        \n
      • 许多 ISP 阻止到端口25(这是邮件接收服务器的默认端口)的出站连接,以防止这种“直接”邮件发送
      • \n
      • 接收服务器方面采取了许多措施,主要是为了防止垃圾邮件,但结果也可能会阻止这种“直接”邮件发送(一些例子是:DNSBL \xe2\x80 \x94 被阻止的 IP 列表,DKIM \xe2\x80\x94 是一种电子邮件身份验证方法,旨在检测电子邮件中的伪造发件人地址(如果您没有自己的合法邮件服务器,您将使用其他人的域对于From字段,这就是您可能会被 DKIM 击中的地方)
      • \n
    • \n
  6. \n
  7. 代码片段没问题。该错误很可能是由于 ISP 方面的阻止而产生的。

  8. \n
\n\n
\n\n

话虽如此,代码片段:

\n\n
import smtplib\n\nfrom email.message import EmailMessage\n\nmessage = EmailMessage()\nmessage.set_content(\'Message content here\')\nmessage[\'Subject\'] = \'Your subject here\'\nmessage[\'From\'] = \'me@example.com\'\nmessage[\'To\'] = \'user@example.com\'\n\nsmtp_server = smtplib.SMTP(\'smtp.server.address:25\')\nsmtp_server.send_message(message)\nsmtp_server.quit()\n
Run Code Online (Sandbox Code Playgroud)\n\n

实际上会发送一封电子邮件(请参阅此问题,了解真实世界的工作示例),因为这smtp.server.address:25是合法的服务器,并且 ISP 和/或smtp.server.address方面没有任何阻止。

\n


Nio*_*bos 5

您对邮件工作原理的理解大致正确。一些其他说明可能会清除问题:

  • SMTP用于两个不同的目的。您似乎混淆了这两个:

    • 第一次使用通常称为“提交”,是将邮件从MUA(邮件用户代理,您的邮件程序,Outlook,Thunderbird等)发送到MTA(邮件传输代理,通常称为“邮件服务器”) 。MTA由您的ISP或邮件提供商(例如GMail)运行。通常,它们的使用受IP地址(仅所述ISP的客户可以使用)或用户名/密码的限制。

    • 第二种用途是将邮件从一个MTA发送到另一MTA。通常,这部分是开放的,因为您可能愿意接受任何人的入站邮件。这也是采取反垃圾邮件措施的位置。

为了发送邮件,至少需要SMTP的第二部分:与另一个MTA进行通信以传递邮件的功能。

发送邮件的典型方法是在应用程序中编写邮件,然后将其发送到MTA邮件服务器进行传递。根据您的设置,MTA可以与Python代码在(localhost)上运行的同一台计算机上安装,也可以是“中央”邮件服务器(可能需要身份验证)。

“您的” MTA将处理传递邮件的所有令人讨厌的细节,例如:

  • 执行DNS查找以找出要联系以中继邮件的MTA。这包括MX查找,还包括其他后备机制,例如A-records

  • 重试传送,如果第一次尝试暂时失败

  • 如果邮件永久失败,则生成退回邮件

  • 如果不同域上有多个收件人,请制作邮件的多个副本

  • 使用DKIM签名消息,以减少将其标记为垃圾邮件的可能性。

  • ...

当然,您可以在自己的Python代码中重新实现所有这些功能,并有效地将MTA与您的应用程序结合在一起,但是我强烈建议不要这样做。邮件出奇地难以正确处理...

底线:尝试通过SMTP将邮件发送到提供商的邮件服务器或其他邮件服务。如果这不可能:如果要运行自己的邮件服务器,请认真考虑。被标记为垃圾邮件制造者很容易发生;从垃圾邮件列表中删除要困难得多。不要在您的应用程序中重新实现SMTP代码。