Discourse SMTP 发送“EHLO localhost”而非域名,导致 Google smtp-relay 故障

一些背景信息:Emails have stopped sending - end of file reached

大约一周前(2021 年 1 月 13 日),通过 Google 的 smtp-relay.gmail.com 服务器发送邮件开始失败(这是 Google Apps 用户被允许且预期的用途)。

Sidekiq 报告了这些失败,错误为 EOFError:

Jobs::HandledExceptionWrapper: Wrapped EOFError: end of file reached

/logs 也报告了失败的任务:

Job exception: end of file reached

其他帖子中提供了完整的回溯信息。

===================

调查表明,最新版本的 Discourse 安装使用 ‘EHLO localhost’ 连接到 SMTP 中继服务器,而 Google 大约在一周前开始拒绝此类请求。

来自生产实例的 tcpdump 输出:

0x0030:  d10f f8e4 4548 4c4f 206c 6f63 616c 686f  ....EHLO.localho
	0x0040:  7374 0d0a                                st..
...
	0x0030:  de62 f0c3 3432 3120 342e 372e 3020 5472  .b..421.4.7.0.Tr
	0x0040:  7920 6167 6169 6e20 6c61 7465 722c 2063  y.again.later,.c
	0x0050:  6c6f 7369 6e67 2063 6f6e 6e65 6374 696f  losing.connectio
	0x0060:  6e2e 2028 4548 4c4f 2920 6a31 3673 6d34  n..(EHLO).j16sm4
	0x0070:  3831 3932 3976 736d 2e31 202d 2067 736d  81929vsm.1.-.gsm
	0x0080:  7470 0d0a                                tp..

使用 telnet 复现也得到相同结果:

root@conversation:~# telnet smtp-relay.gmail.com 587
Trying 74.125.137.28...
Connected to smtp-relay.gmail.com.
Escape character is '^]'.
220 smtp-relay.gmail.com ESMTP ls8sm507258pjb.6 - gsmtp
ehlo localhost.localdomain
421 4.7.0 Try again later, closing connection. (EHLO) ls8sm507258pjb.6 - gsmtp
Connection closed by foreign host.

然而,使用特定域名的 ehlo 则正常工作:

root@conversation:~# telnet smtp-relay.gmail.com 587
Trying 74.125.137.28...
Connected to smtp-relay.gmail.com.
Escape character is '^]'.
220 smtp-relay.gmail.com ESMTP p10sm668563uaw.3 - gsmtp
ehlo conversation.sevarg.net
250-smtp-relay.gmail.com at your service, [64.227.96.27]
250-SIZE 157286400
250-8BITMIME
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8

======

根据日志,我确定了需要修改的文件以测试修复方案(在 Docker 镜像中):

/var/www/discourse/vendor/bundle/ruby/2.7.0/gems/mail-2.7.1/lib/mail/network/delivery_methods/smtp.rb

DEFAULTS = {
      :address              => 'localhost',
      :port                 => 25,
      :domain               => 'localhost.localdomain',

修改为

    DEFAULTS = {
      :address              => 'conversation.sevarg.net',
      :port                 => 25,
      :domain               => 'conversation.sevarg.net',

问题得以解决(在重启实例后)。现在 EHLO 使用的是域名字符串,邮件也能从我的实例正常发送了。

================

期望行为:发送邮件时,默认的 Discourse 安装应使用配置的域名作为与 SMTP 服务器初始连接时的 EHLO 参数。或者,应提供一个配置选项以覆盖发送的域名。如果存在该选项,我通过搜索未能找到。

5 个赞

我相信我也见过其他人遇到同样的错误(他们可能也没有使用 Google Domains)。

一个长期的解决方案是在你的 app.yml 中添加一些自动重写规则。但希望会有真正的修复方案推出。

如果可以通过 app.yml 来修复,我当然很感兴趣——在代码中硬编码域名以实现邮件功能显然不是一个“正确”的解决方案,但它确实指明了更永久地解决问题的方向。

是否有原因导致它不直接使用配置的域名作为 ehlo?这样做比使用 localhost 更“正确”。

出色的调查工作 @Syonyk

能否请您分享您的 app.yml 文件中的 SMTP 设置?

2 个赞

那里除了正常的必需设置外什么都没有。

  DISCOURSE_SMTP_ADDRESS: smtp-relay.gmail.com
  DISCOURSE_SMTP_PORT: 587
  DISCOURSE_SMTP_USER_NAME: [邮箱用户名]
  DISCOURSE_SMTP_PASSWORD: [密码]

能否请您尝试添加一行:

DISCOURSE_SMTP_DOMAIN: conversation.sevarg.net

然后再次尝试?

3 个赞

我添加了那一行并重新构建了应用(有没有办法绕过这一步?)。

现在我在邮件设置中看到了“domain”选项,smtp.rb 文件已恢复为默认使用 localhost,邮件似乎也发送正常——我可以发送测试消息,并且它们被正确传输了。

据我所知,问题已解决。能否将此内容添加到文档或设置流程中的某个位置?我找了很久这样的设置,但没找到这个选项——即使知道有这个配置项,相关说明也极少。

2 个赞

它可以在默认的 app.yml 示例文件的这个代码块中添加:

您认为这有帮助吗?

1 个赞

只要文档中有记录,我就满意。

不过,如果未设置该值,代码能否使用 DISCOURSE_HOSTNAME 的值(在几乎所有简单情况下这都是正确的)?在 EHLO 中发送 ‘localhost’ 通常是错误的(至少在与非 localhost 的服务器通信时)。

我认为将其添加到 standalone.ymlweb_only.yml 是个好主意。

我同意它可能应该默认使用 DISCOURSE_HOSTNAME。这显然比 localhost 更好。最近有变化吗?

问题在于,目前对于成千上万不依赖 Google 发送电子邮件的 Discourse 实例来说,当前的设置是有效的。更改这样的默认值可能会导致所有用户受到影响,而仅仅修复相对较少的 Google Apps 用户的问题。

我会将其添加到示例中,我们还应该创建一个名为“使用 Google Apps 发送外部邮件”的 howto 主题来记录这一点。有人可以接手处理吗?

4 个赞

好的。我同意,谨慎行事可能是明智的,但我猜测,任何能接受 localhost 的邮件服务器应该也能接受 DISCOURSE_HOSTNAME,不过我并没有相关的数据支持。:wink: 将其包含在标准模板中,可能已经足够好了。

1 个赞

是的,但向远程主机发送“localhost”也是错误的,这违反了 RFC 规范。

重点在我这里。

旧版 RFC 指出服务器不应基于 EHLO 字符串拒绝客户端,Google 似乎正在这样做,但我没在 5321 中看到相关措辞。

我预期任何容忍 localhost 的远程邮件服务器也会容忍(并更倾向于)RFC 要求的完全限定域名(FQDN)。我理解大家不想破坏现有功能的愿望,但根据我对相关 RFC 的解读,Discourse 的默认设置本身就是错误的,之所以能工作,仅仅是因为远程 SMTP 服务器过于宽松。

1 个赞

我很乐意合并一个针对 ./discourse-setup 的 PR,将其默认设置为与提供的 DISCOURSE_HOSTNAME 相同,前提是在我们建议用户使用的最常见 SMTP 服务中已证明其无害。

2 个赞

我无法测试完整的端到端邮件投递,因为我没有相关账户,但情况如下:

Mailgun

% nc smtp.mailgun.com 587
220 Mailgun Influx ready
ehlo conversation.sevarg.net
250-smtp-out-n04.prod.us-west-2.postgun.com
250-AUTH PLAIN LOGIN
...

Sendgrid

% nc smtp.sendgrid.net 587
220 SG ESMTP service ready at ismtpd0021p1las1.sendgrid.net
ehlo conversation.sevarg.net
250-smtp.sendgrid.net
250-8BITMIME
...

Mailjet

 % nc smtp.mailjet.com 587
220 in.mailjet.com ESMTP Mailjet
ehlo conversation.sevarg.net
250-smtpin.mailjet.com
250-PIPELINING
...

ElasticMail

……实际上它对任何形式的 helo 或 ehlo 命令都不响应。o.O 无论是 localhost 还是任何真实域名。

我认为在设置阶段进行配置是正确的做法,因为这样至少能让用户知晓并可根据需要进行修改。

6 个赞

还有一个相关问题:discourse-doctor 似乎没有正确设置域名,即使在实际安装能够发送邮件时,它仍然会失败。

在使用正常配置的情况下,discourse-doctor 仍然报告文件末尾错误。

======================================== ERROR ========================================
                                    UNEXPECTED ERROR

end of file reached

====================================== SOLUTION =======================================
这不是一个常见错误。没有推荐的解决方案!

请将上述确切的错误信息报告至 https://meta.discourse.org/
(如果你找到了解决方案,也请一并报告!)
=======================================================================================

测试脚本中并未提及 SMTP_DOMAIN。

root@conversation:/var/discourse# grep SMTP_DOMAIN discourse-doctor
root@conversation:/var/discourse#

tcpdump 显示,运行 discourse-doctor 时仍然在 EHLO 中发送 ‘localhost’。这也需要修复。

	0x0030:  cccd b12c 4548 4c4f 206c 6f63 616c 686f  ...,EHLO.localho
	0x0040:  7374 0d0a                                st..
...
	0x0030:  e247 1aa5 3432 3120 342e 372e 3020 5472  .G..421.4.7.0.Tr
	0x0040:  7920 6167 6169 6e20 6c61 7465 722c 2063  y.again.later,.c
	0x0050:  6c6f 7369 6e67 2063 6f6e 6e65 6374 696f  losing.connectio
	0x0060:  6e2e 2028 4548 4c4f 2920 6e6d 3773 6d31  n..(EHLO).nm7sm1
	0x0070:  3032 3832 3139 706a 622e 3620 2d20 6773  028219pjb.6.-.gs
	0x0080:  6d74 700d 0a                             mtp..

那不是 discourse-doctor,而是 emails.rake

啊,看来它使用的是 localhost。我想它应该引用 #{ENV["DISCOURSE_SMTP_PORT"]}——或者暂时使用 DISCOURSE_SMTP_DOMAIN

@falco 我们确定这一直如此,而不是近期的回归问题吗?

我记得很久以前就存在与发送 ehlo {invalid domain} 相关的问题,因此,如果我们在很长一段时间内一直错误地发送 ehlo localhost,我会感到非常惊讶。

1 个赞

抱歉,我只是报告我所看到的情况。我相当确定那里的 ‘localhost’ 也是错的。我的 Web 开发经验主要比较陈旧,专业上已经十多年没碰过这些东西了。能走到这一步花了不少时间,Docker、Ruby 之类的对我来说都还算新。不过 tcpdump 嘛……那工具我可是很熟的。:wink:

不过我仍然认为,在任何情况下把 ‘localhost’ 发送给远程服务器都是错误的行为。

1 个赞

仅供参考,我们从1月13日起也遇到了完全相同的问题。最后一封成功发送的邮件是在1月13日,我们同样使用的是 smtp-relay.gmail.com —— 目前尚未找到绕过此问题的方法(除非修改源代码)。