使用 HTTP POST 路由将 Mailgun 的传入电子邮件转发到自己的 SMTP 服务器时出现问题

Problems forwarding Mailgun's incoming email to own SMTP server using HTTP POST route

最近我不得不使用 CloudFlare 隐藏我的 IP 地址,但是我的 MX 记录指向我自己的域,我不能。

我知道 Mailgun 可以将所有发给 user@example.com 的邮件转发到 Gmail,但我不想那样。希望邮件可以转发example.com自己的SMTP服务器

我决定使用 Mailgun catch_all()forward("https://exmaple.com/incoming-email?key=SECRET_KEY"),并为此编写了一些 node.js 代码。

var nodemailer = require('nodemailer');
var express = require('express');
var multer = require('multer');
var router = express.Router();

var KEY = 'SECRET_KEY';
var ADDITIONAL_HEADERS = ['timestamp', 'token', 'signature', 'mime-version'];
var transporter = nodemailer.createTransport('smtp://127.0.0.1');

var storage = multer.memoryStorage();
var upload = multer({
  storage: storage
});

router.post('/', upload.any(), function (req, res, next) {
  if (!req.query.key || req.query.key !== KEY) {
    res.sendStatus(403);
    return;
  }
  console.log('Incoming mail: ', req.body.From);
  req.body.to = req.body.To;
  req.body.cc = req.body.Cc;
  req.body.from = req.body.From;
  req.body.date = req.body.Date;
  req.body.references = req.body.References;
  req.body.replyTo = req.body['Reply-To'];
  req.body.inReplyTo = req.body['In-Reply-To'];
  req.body.messageId = req.body['Message-Id'];
  req.body.html = req.body['body-html'];
  req.body.text = req.body['body-plain'];
  req.body.headers = {};
  for (var key in req.body) {
    if (key.toLowerCase().indexOf('X-') !== -1 ||
      ADDITIONAL_HEADERS.indexOf(key.toLowerCase()) !== -1) {
      req.body.headers[key] = req.body[key];
    }
  }
  if (req.files) {
    req.body.attachments = req.files;
    req.body.attachments.forEach(function (attachment) {
      attachment.filename = attachment.originalname;
      attachment.contentType = attachment.mimetype;
      attachment.content = attachment.buffer;
    });
  }
  transporter.sendMail(req.body, function (err, info) {
    if (err) {
      console.log(err);
      res.status(400).send(err);
      return;
    }
    console.log(info);
    res.send(info);
  });
});

module.exports = router;

如您所见,该代码能够将任何传入的电子邮件转发到本地主机 SMTP 服务器。使用 about 代码,我可以使用 user@example.com 接收电子邮件。收到的电子邮件将有正确的发件人、收件人、抄送、主题和正文,甚至附件。

然而,一天后我被迫从 Arch Linux 邮件列表中退订,一个人给我发邮件说:

It appears that your mail server is misconfigured and sends at least some mails it receives from our mailing lists back to the list. It also removes some headers in the process which help mailman to detect such behaviour. Since those headers are missing, mailman will send the mail out once again to everyone, leading to a never ending loop.

在我求助后,也收到一些邮件说我在这期间收不到邮件列表的任何邮件。

为什么会出错?在转发/包装过程中删除的电子邮件中的 "some headers" 是什么?是Mailgun的问题还是我自己的问题?如果是我的问题,可以在上面的代码中添加什么?谢谢。

一封电子邮件不仅有收件人 header,还有 SMTP RCPT TO 值。此值可能与电子邮件中包含的收件人 header 不同。

参见:In SMTP, must the RCPT TO: and TO: match?

您的代码似乎使用了邮件内容,似乎没有单独处理传输收件人 (SMTP RCPT TO)。不知道mailgun有没有提供这个值。

只需将邮件提交到您的 smtp 服务器,一些图书馆可能会从收件人 header 中提取收件人,但是对于邮寄列表,这通常是不正确的。

我发现当电子邮件是密送给您时,邮件将看起来像:

  • 致:announce@example-mailing-list.com
  • 密件抄送:你@example.com

这样当使用问题中的代码时,邮件将由 node.js 代码处理,然后邮件将使用 [=11] 发送回 announce@example-mailing-list.com =], 导致无限循环。

我更新了我的代码以从收件人列表中找到我的域的所有电子邮件地址并将它们设置为 To 字段,无限循环不再出现。

例如,收件人列表看起来像:announce@example-mailing-list.com, you@example.com,所以我找到所有以 @example.com 结尾的电子邮件地址并将它们设为 To 字段。

警告:虽然有些信息会丢失,比如我无法再看到电子邮件的原始 To 字段。但是,您可以将原始 To 字段附加到任何字段,例如 subject.

var nodemailer = require('nodemailer');
var express = require('express');
var multer = require('multer');
var router = express.Router();

var MY_DOMAIN = 'example.com';
var KEY = 'SECRET_KEY;
var ADDITIONAL_HEADERS = ['timestamp', 'token', 'signature', 'mime-version',
  'return-path', 'dkim-signature'
];
var transporter = nodemailer.createTransport('smtp://127.0.0.1');

var storage = multer.memoryStorage();
var upload = multer({
  storage: storage
});

router.post('/', upload.any(), function (req, res, next) {
  if (!req.query.key || req.query.key !== KEY) {
    res.sendStatus(403);
    return;
  }
  console.log('Incoming mail: ', req.body.From);
  req.body.to = req.body.recipient.split(', ').filter(email => email.indexOf('@' + MY_DOMAIN) !== -1);
  req.body.from = req.body.From;
  req.body.date = req.body.Date;
  req.body.references = req.body.References;
  req.body.replyTo = req.body['Reply-To'];
  req.body.inReplyTo = req.body['In-Reply-To'];
  req.body.messageId = req.body['Message-Id'];
  req.body.html = req.body['body-html'];
  req.body.text = req.body['body-plain'];
  req.body.headers = {};
  for (var key in req.body) {
    if (key.toLowerCase().indexOf('x-') !== -1 ||
      ADDITIONAL_HEADERS.indexOf(key.toLowerCase()) !== -1) {
      req.body.headers[key] = req.body[key];
    }
  }
  console.log('Params: ' + JSON.stringify(req.body));
  if (req.files) {
    req.body.attachments = req.files;
    req.body.attachments.forEach(function (attachment) {
      attachment.filename = attachment.originalname;
      attachment.contentType = attachment.mimetype;
      attachment.content = attachment.buffer;
    });
  }
  transporter.sendMail(req.body, function (err, info) {
    if (err) {
      console.log(err);
      res.status(400).send(err);
      return;
    }
    console.log(info);
    res.send(info);
  });
});

module.exports = router;