跳到正文
阿里雲開發者社區 AI·· 3 小時前AI 評分43

WordPress 聯絡表單收不到查詢、郵件進入垃圾郵件或遭機器人刷:按順序排查

WordPress 联系表单收不到询盘、进垃圾箱、被机器人刷:按这个顺序排查

AI 導讀

WordPress 聯絡表單故障可分為提交、發信、送達和機器人過濾等層,應依次驗證,避免把不同問題混在一起排查。先確認提交有否成功並儲存查詢,再檢查 wp_mail()、SMTP 及 SPF、DKIM、DMARC 認證。防機器人可結合蜜罐、時間戳、限流和內容檢查,並以資料庫記錄作為查詢來源,避免郵件失敗造成查詢遺失。

正文

2026-10-23 5

版权

版权声明:

本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《 阿里云开发者社区用户服务协议》和 《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写 侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

现象看着都像「表单坏了」,其实是四层完全不同的故障。顺序搞反,就会一直在改模板,而问题在发信域。

先分类:你遇到的是哪一层

现象 卡住的层 第一个要看的指标
后台没有任何记录,访客说提交了 提交层(前端 / 路由 / nonce) 浏览器 Network 面板有没有 200
数据库 / 日志里有记录,邮件没收着 发信层(wp_mail / SMTP) wp_mail_failed 的报错
邮箱收到了,但在垃圾箱或「促销」里 送达层(SPF / DKIM / DMARC / 内容) 原始邮件头里的 Authentication-Results
收件箱一堆带链接的英文询盘 过滤层(机器人) 提交间隔、字段内容特征

下面按这个顺序走,每一步都能单独验证,不要跳步。

步骤 1:先确认表单到底有没有提交成功

最常见的坑:整个表单只有「发邮件」这一条路。 邮件失败,询盘就彻底消失了,你连「有没有人提交过」都不知道。

先改成「先落库,再发信」。存成自定义文章类型或者独立表都行,关键是后台能看到。

然后不开浏览器,直接用 curl 打你自己的接口,把提交层和邮件层拆开看:

curl -i -X POST 'https://example.com/wp-admin/admin-ajax.php' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'action=aum_inquiry' \
  --data-urlencode 'nonce=换成真实nonce' \
  --data-urlencode 'name=Curl Test' \
  --data-urlencode 'email=test@example.com' \
  --data-urlencode 'message=This is a test message sent by curl.' \
  --data-urlencode "ts=$(date +%s)"

返回值里带上 stored 和 mailed 两个布尔值,一眼就知道断在哪一层。如果 curl 返回 403,问题在 nonce 或安全插件;返回 200 但数据库没记录,问题在落库逻辑。

同时看服务端日志:

tail -f /var/log/nginx/error.log
tail -f wp-content/debug.log

步骤 2:确认 wp_mail() 有没有真的发出去

wp_mail() 返回 false 只是结果,原因在 wp_mail_failed 里。扔一个 mu-plugin 进去记录:

<?php
// wp-content/mu-plugins/aum-mail-log.php
add_action('wp_mail_failed', function (WP_Error $e) {
   
    $d = (array) $e->get_error_data();
    error_log(sprintf(
        '[mail-fail] %s | to=%s | subject=%s',
        $e->get_error_message(),
        implode(',', (array) ($d['to'] ?? [])),
        (string) ($d['subject'] ?? '')
    ));
});

add_filter('wp_mail', function ($args) {
   
    error_log('[mail-try] to=' . implode(',', (array) $args['to']) . ' subject=' . $args['subject']);
    return $args;
});

常见报错与对策:

  • Could not instantiate mail function —— 主机禁了 mail(),必须走 SMTP。
  • SMTP connect() failed —— 端口被墙或凭据错。
  • Message body empty —— 模板渲染出了空字符串,查一下变量。
  • 没有任何报错也没收到 —— 大概率发出去了,去下一步。

主机自带的 sendmail 只适合调试。 共享主机的出口 IP 经常和大量垃圾邮件共用一个段,进垃圾箱几乎是必然。生产环境用 SMTP 或发信 API,并把发信日志留在自己手里。

步骤 3:查发信域认证,这一层最容易被忽略

在收件邮箱里打开「显示原始邮件」,找这三行:

authentication-results: spf=pass; dkim=pass; dmarc=pass

三个里有一个 fail,进垃圾箱就不奇怪。对应的 DNS 记录:

example.com.          TXT  "v=spf1 a mx include:_spf.your-provider.example ~all"
mail._domainkey.example.com.  TXT  "v=DKIM1; k=rsa; p=公钥内容"
_dmarc.example.com.   TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com"

几点硬要求:

  1. SPF 一个域只能有一条 TXT 记录,多条会直接 fail。所有发信服务合并到同一条里。
  2. DMARC 先从 p=none 开始,收报告观察,别一上来就 reject。
  3. 发信 IP 要有 PTR 反解,云主机默认经常没有。
  4. From 必须是你的域名,访客地址放 Reply-To。 直接把 From 写成访客的邮箱(From: visitor@gmail.com)属于伪造发件人,SPF 一定对不上。
$headers = [
    'From: Example Web <no-reply@example.com>',   // 必须是本站域名
    'Reply-To: ' . $name . ' <' . $email . '>',   // 访客地址放这里
    'Content-Type: text/plain; charset=UTF-8',
];

步骤 4:内容层别自己把自己送进垃圾箱

  • 正文全是图片、只有一句「Please contact us」、或者只剩一个短链 —— 分数很低。
  • 一封邮件里塞五六个外链、短链、$ !!! 100% FREE 这类词 —— 直接掉分。
  • 主题行直接把用户输入拼进去,用户输入里带换行就是 header injection,整封信可能被判为恶意。

清洗函数,所有进 header 的字段都要过一遍:

function aum_clean_header($v): string {
   
    return trim(str_replace(["\r", "\n", "%0a", "%0d", "%0A", "%0D"], '', (string) $v));
}

正文用 sanitize_textarea_field(),邮箱用 sanitize_email() 后再 is_email() 校验。别自己写正则过滤 HTML。

步骤 5:机器人,分层挡,不要只靠一个验证码

单靠验证码的问题是可访问性和误杀,而且现在不少自动化脚本能过简单图形码。实际有效的是下面几层叠加,任何一层命中就静默丢弃:

<?php
// wp-content/mu-plugins/aum-inquiry.php
add_action('wp_ajax_nopriv_aum_inquiry', 'aum_handle_inquiry');
add_action('wp_ajax_aum_inquiry', 'aum_handle_inquiry');

function aum_handle_inquiry(): void {
   
    // 第 1 层:nonce
    if (!check_ajax_referer('aum_inquiry', 'nonce', false)) {
   
        wp_send_json_error(['code' => 'bad_nonce'], 403);
    }

    // 第 2 层:蜜罐。正常用户看不见这个字段
    if (!empty($_POST['website'])) {
   
        wp_send_json_success(['ok' => true]); // 静默成功,不给机器人重试信号
    }

    // 第 3 层:时间戳。渲染到提交少于 3 秒,或超过 2 小时
    $ts  = (int) ($_POST['ts'] ?? 0);
    $age = time() - $ts;
    if ($ts <= 0 || $age < 3 || $age > 7200) {
   
        wp_send_json_error(['code' => 'bad_timing'], 400);
    }

    // 第 4 层:限流。同一 IP 10 分钟最多 3 次
    $ip  = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    $key = 'aum_rl_' . md5($ip);
    $hit = (int) get_transient($key);
    if ($hit >= 3) {
   
        wp_send_json_error(['code' => 'rate_limited'], 429);
    }
    set_transient($key, $hit + 1, 10 * MINUTE_IN_SECONDS);

    // 第 5 层:字段与内容规则
    $name    = aum_clean_header($_POST['name'] ?? '');
    $email   = sanitize_email($_POST['email'] ?? '');
    $message = sanitize_textarea_field($_POST['message'] ?? '');

    if ($name === '' || !is_email($email) || mb_strlen($message) < 10) {
   
        wp_send_json_error(['code' => 'invalid'], 400);
    }
    if (preg_match_all('#https?://#i', $message) > 2) {
   
        wp_send_json_error(['code' => 'too_many_links'], 400);
    }

    // 先落库,再发信
    $post_id = wp_insert_post([
        'post_type'   => 'aum_inquiry',
        'post_status' => 'private',
        'post_title'  => $name . ' <' . $email . '>',
        'post_content'=> $message,
    ], true);

    $sent = wp_mail(
        'sales@example.com',
        'New inquiry from ' . $name,
        $message . "\n\nIP: " . $ip,
        ['From: Example Web <no-reply@example.com>', 'Reply-To: ' . $name . ' <' . $email . '>']
    );

    wp_send_json_success(['ok' => true, 'stored' => !is_wp_error($post_id), 'mailed' => (bool) $sent]);
}

前端配合最小的三行:

<input type="text" name="website" tabindex="-1" autocomplete="off"
       style="position:absolute;left:-9999px" aria-hidden="true">
<input type="hidden" name="ts" value="<?php echo esc_attr(time()); ?>">

几个容易踩的点:

  • 蜜罐命中要返回成功。返回 403 会让脚本换策略重试,静默丢弃更省事。
  • 时间戳要能防篡改。如果被人直接构造请求,加一层签名:wp_hash($ts . 'aum_inquiry'),服务端 hash_equals() 比对。
  • 限流键不要只用 IP。公司出口、手机网络会出现多人同 IP,可以叠加 UA 或会话 ID 做二级键。
  • 不要在页面上暴露「为什么被拦」。错误码只给个泛化文案。

如果刷单量已经影响到正常询盘,可以在服务器前面那层按路径做频率限制,把 /wp-admin/admin-ajax.php 的 POST 单独限速,比在 PHP 里挡更省资源。

步骤 6:兜底设计,让「丢询盘」这件事不可能发生

  • 询盘先入库,后台有一个可搜索的列表;邮件只是通知,不是数据源。
  • wp_mail 失败时给管理员发站内通知或者写一条高优先级日志,别静默失败。
  • 表单旁边保留第二个入口。对话式接询盘可以少一次「填完没回音」的流失——AumChat 把规则、AI 和真人在同一条对话里接力,公开演示在 chat.aumcreate.com/demo,不用注册就能看到店主那一侧的工作台;客户端在后台也会提醒你。装到 WordPress 上用的是 AumChat 安装器:https://aumcreate.com/plugins/aumchat?utm_source=aliyun&utm_medium=content&utm_campaign=aliyun-wp-form-spam-missing。

一页速查

症状 先做这个 不要做这个
提交后无任何记录 落库 + curl 打接口 换模板
有记录但没邮件 打开 wp_mail_failed 日志 反复点提交
邮件进垃圾箱 查 Authentication-Results 三行 改邮件正文措辞
From 用了访客邮箱 改成本站域名 + Reply-To 继续用默认设置
收到大量带链接的英文 蜜罐 + 时间戳 + 限流 只加一个验证码

排查的顺序永远是:提交 → 发信 → 认证 → 内容 → 机器人。前一层没验证通过,就不要动后面一层,否则你会同时改五处,最后不知道是哪一处生效的。

如果站点现有表单结构比较乱,想一次性把落库、发信、认证、限流都配到位,我们的代部署服务 $399、5 个工作日:https://aumcreate.com/setup?utm_source=aliyun&utm_medium=content&utm_campaign=aliyun-wp-form-spam-missing。

來源:阿里雲開發者社區 AI · developer.aliyun.com