把Twikoo 默认的nodemail发邮件换成对接php邮件接口

-
-
2024-08-06

牢骚

特么的,自己服务器搭建的邮件服务器各种好使,随便用,一到twikoo就特么提示localhost不在证书信息内,把服务器的hosts去了127.0.0.1 localhost都不好使

 我去踏马的nodemail,傻X玩意写的。

一直对前端各种JS框架还有还有所谓的后端node.js不感冒,感觉实在是把问题复杂化,可能是我还没有体验到Node.js带来的性能提升吧,一直搞的站都不大,PHP真踏马好用。

当然这里要排除掉trilium,哈哈,这是我真爱。

实现方法

牢骚发完了,该整的还是要整,我就不想用别的平台的邮件发送,我就想用自己搭建的。我也懒得去深入研究nodemail。

想着,twikoo发送邮件既然是调用nodemail,那我发它的调用改成发往php接口不就好了。以下是实现方法:

1.修改twikoo 发送邮件接入点

twikoo目录/node_modules/twikoo-func/utils/notify.js  改成如下:

const { equalsMail, getAvatar } = require('.')
const {
  getCheerio,
  getNodemailer,
  getPushoo
} = require('./lib')
const $ = getCheerio()
const pushoo = getPushoo()
const { RES_CODE } = require('./constants')
const logger = require('./logger')
const axios = require('axios');
// PHP API端点 这里要改成你自己的API地址
const PHP_API_ENDPOINT = 'https://sendmail.api.com/'; //这里一定要加/,不然重定向后Post请求就变成了GET
let nodemailer

function lazilyGetNodemailer () {
  return nodemailer ?? (nodemailer = getNodemailer())
}

// 用于发送邮件的函数
async function sendMailPhpApi(options) {
  try {
    // 使用axios发送POST请求到PHP API
    const response = await axios.post(PHP_API_ENDPOINT, options, {
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    // 根据API的响应结构处理结果
    if (response.data.success) {
      console.log('邮件发送成功');
    } else {
      console.error('邮件发送失败:', response.data.message);
    }
    
    return response.data;
  } catch (error) {
    console.error('邮件发送请求失败:', error);
    throw error;
  }
}


let transporter = {
      sendMail: async (options) => {
        try {
          // 使用您的PHP API发送邮件函数
          const response = await sendMailPhpApi(options);
          if (response && response.success) {
            return response; // 根据实际响应结构调整
          } else {
            throw new Error('邮件发送失败: ' + (response ? response.message : '未知错误'));
          }
        } catch (error) {
          logger.error('邮件发送请求失败:', error);
          throw error;
        }
      }
    };

const fn = {
  // 发送通知
  async sendNotice (comment, config, getParentComment) {
    if (comment.isSpam && config.NOTIFY_SPAM === 'false') return
    await Promise.all([
      fn.noticeMaster(comment, config),
      fn.noticeReply(comment, config, getParentComment),
      fn.noticePushoo(comment, config)
    ]).catch(err => {
      logger.error('通知异常:', err)
    })
  },
  // 初始化邮件插件
  async initMailer ({ config, throwErr = false } = {}) {
    try {
      if (!config || !config.SMTP_USER || !config.SMTP_PASS) {
        throw new Error('数据库配置不存在')
      }
      const transportConfig = {
        auth: {
          user: config.SMTP_USER,
          pass: config.SMTP_PASS
        }
      }
      if (config.SMTP_SERVICE) {
        transportConfig.service = config.SMTP_SERVICE
      } else if (config.SMTP_HOST) {
        transportConfig.host = config.SMTP_HOST
        transportConfig.port = parseInt(config.SMTP_PORT)
        transportConfig.secure = config.SMTP_SECURE === 'true'
      } else {
        throw new Error('SMTP 服务器没有配置')
      }
      transporter = transporter;
      return true
    } catch (e) {
      if (throwErr) {
        logger.error('邮件初始化异常:', e.message)
        throw e
      } else {
        logger.warn('邮件初始化异常:', e.message)
      }
      return false
    }
  },
  // 博主通知
  async noticeMaster (comment, config) {
    if (!transporter && !await fn.initMailer({ config })) {
      logger.info('未配置邮箱或邮箱配置有误,不通知')
      return
    }
    if (equalsMail(config.BLOGGER_EMAIL, comment.mail)) {
      logger.info('博主本人评论,不发送通知给博主')
      return
    }
    // 判断是否存在即时消息推送配置
    const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
    if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') {
      logger.info('存在即时消息推送配置,默认不发送邮件给博主,您可以在管理面板修改此行为')
      return
    }
    const SITE_NAME = config.SITE_NAME
    const NICK = comment.nick
    const IMG = getAvatar(comment, config)
    const IP = comment.ip
    const MAIL = comment.mail
    const COMMENT = comment.comment
    const SITE_URL = config.SITE_URL
    const POST_URL = fn.appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
    const emailSubject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}上有新评论了`
    let emailContent
    if (config.MAIL_TEMPLATE_ADMIN) {
      emailContent = config.MAIL_TEMPLATE_ADMIN
        .replace(/\${SITE_URL}/g, SITE_URL)
        .replace(/\${SITE_NAME}/g, SITE_NAME)
        .replace(/\${NICK}/g, NICK)
        .replace(/\${IMG}/g, IMG)
        .replace(/\${IP}/g, IP)
        .replace(/\${MAIL}/g, MAIL)
        .replace(/\${COMMENT}/g, COMMENT)
        .replace(/\${POST_URL}/g, POST_URL)
    } else {
      emailContent = `
        <div style="border-top:2px solid #12addb;box-shadow:0 1px 3px #aaaaaa;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;">
          <h2 style="border-bottom:1px solid #dddddd;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;">
            您在<a style="text-decoration:none;color: #12addb;" href="${SITE_URL}" target="_blank">${SITE_NAME}</a>上的文章有了新的评论
          </h2>
          <p><strong>${NICK}</strong>回复说:</p>
          <div style="background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;">${COMMENT}</div>
          <p>您可以点击<a style="text-decoration:none; color:#12addb" href="${POST_URL}" target="_blank">查看回复的完整內容</a><br></p>
        </div>`
    }
    let sendResult
    try {
      sendResult = await transporter.sendMail({
        from: `"${config.SENDER_NAME}" <${config.SENDER_EMAIL}>`,
        to: config.BLOGGER_EMAIL || config.SENDER_EMAIL,
        subject: emailSubject,
        html: emailContent
      })
    } catch (e) {
      sendResult = e
    }
    logger.log('博主通知结果:', sendResult)
    return sendResult
  },
  // 即时消息通知
  async noticePushoo (comment, config) {
    if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
      logger.info('没有配置 pushoo,放弃即时消息通知')
      return
    }
    if (equalsMail(config.BLOGGER_EMAIL, comment.mail)) {
      logger.info('博主本人评论,不发送通知给博主')
      return
    }
    const pushContent = fn.getIMPushContent(comment, config)
    const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
      token: config.PUSHOO_TOKEN,
      title: pushContent.subject,
      content: pushContent.content,
      options: {
        bark: {
          url: pushContent.url
        }
      }
    })
    logger.info('即时消息通知结果:', sendResult)
  },
  // 即时消息推送内容获取
  getIMPushContent (comment, config) {
    const SITE_NAME = config.SITE_NAME
    const NICK = comment.nick
    const MAIL = comment.mail
    const IP = comment.ip
    const COMMENT = $(comment.comment).text()
    const SITE_URL = config.SITE_URL
    const POST_URL = fn.appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
    const subject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}有新评论了`
    const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))

评论人IP:${IP}

评论内容:${COMMENT}

原文链接:[${POST_URL}](${POST_URL})`
    return {
      subject,
      content,
      url: POST_URL
    }
  },
  // 回复通知
  async noticeReply (currentComment, config, getParentComment) {
    if (!currentComment.pid) {
      logger.info('无父级评论,不通知')
      return
    }
    if (!transporter && !await fn.initMailer({ config })) {
      logger.info('未配置邮箱或邮箱配置有误,不通知')
      return
    }
    const parentComment = await getParentComment(currentComment)
    if (equalsMail(config.BLOGGER_EMAIL, parentComment.mail)) {
      logger.info('回复给博主,因为会发博主通知邮件,所以不再重复通知')
      return
    }
    if (equalsMail(currentComment.mail, parentComment.mail)) {
      logger.info('回复自己的评论,不邮件通知')
      return
    }
    const PARENT_NICK = parentComment.nick
    const IMG = getAvatar(currentComment, config)
    const PARENT_IMG = getAvatar(parentComment, config)
    const SITE_NAME = config.SITE_NAME
    const NICK = currentComment.nick
    const COMMENT = currentComment.comment
    const PARENT_COMMENT = parentComment.comment
    const POST_URL = fn.appendHashToUrl(currentComment.href || config.SITE_URL + currentComment.url, currentComment.id)
    const SITE_URL = config.SITE_URL
    const emailSubject = config.MAIL_SUBJECT || `${PARENT_NICK},您在『${SITE_NAME}』上的评论收到了回复`
    let emailContent
    if (config.MAIL_TEMPLATE) {
      emailContent = config.MAIL_TEMPLATE
        .replace(/\${IMG}/g, IMG)
        .replace(/\${PARENT_IMG}/g, PARENT_IMG)
        .replace(/\${SITE_URL}/g, SITE_URL)
        .replace(/\${SITE_NAME}/g, SITE_NAME)
        .replace(/\${PARENT_NICK}/g, PARENT_NICK)
        .replace(/\${PARENT_COMMENT}/g, PARENT_COMMENT)
        .replace(/\${NICK}/g, NICK)
        .replace(/\${COMMENT}/g, COMMENT)
        .replace(/\${POST_URL}/g, POST_URL)
    } else {
      emailContent = `
        <div style="border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;">
          <h2 style="border-bottom:1px solid #dddddd;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;">
            您在<a style="text-decoration:none;color: #12ADDB;" href="${SITE_URL}" target="_blank">${SITE_NAME}</a>上的评论有了新的回复
          </h2>
          ${PARENT_NICK} 同学,您曾发表评论:
          <div style="padding:0 12px 0 12px;margin-top:18px">
            <div style="background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;">${PARENT_COMMENT}</div>
            <p><strong>${NICK}</strong>回复说:</p>
            <div style="background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;">${COMMENT}</div>
            <p>
              您可以点击<a style="text-decoration:none; color:#12addb" href="${POST_URL}" target="_blank">查看回复的完整內容</a>,
              欢迎再次光临<a style="text-decoration:none; color:#12addb" href="${SITE_URL}" target="_blank">${SITE_NAME}</a>。<br>
            </p>
          </div>
        </div>`
    }
    let sendResult
    try {
      sendResult = await transporter.sendMail({
        from: `"${config.SENDER_NAME}" <${config.SENDER_EMAIL}>`,
        to: parentComment.mail,
        subject: emailSubject,
        html: emailContent
      })
    } catch (e) {
      sendResult = e
    }
    logger.log('回复通知结果:', sendResult)
    return sendResult
  },
  appendHashToUrl (url, hash) {
    if (url.indexOf('#') === -1) {
      return `${url}#${hash}`
    } else {
      return `${url.substring(0, url.indexOf('#'))}#${hash}`
    }
  },
  async emailTest (event, config, isAdminUser) {
    const res = {}
    if (isAdminUser) {
      try {
        // 邮件测试前清除 transporter,保证读取的是最新的配置
        //transporter = null
        //await fn.initMailer({ config, throwErr: true })
        const sendResult = await transporter.sendMail({
          from: `"${config.SENDER_NAME}" <${config.SENDER_EMAIL}>`,
          to: event.mail || config.BLOGGER_EMAIL || config.SENDER_EMAIL,
          subject: 'Twikoo 邮件通知测试邮件',
          html: '如果您收到这封邮件,说明 Twikoo 邮件功能配置正确'
        })
        res.result = sendResult
      } catch (e) {
        res.message = e.message
      }
    } else {
      res.code = RES_CODE.NEED_LOGIN
      res.message = '请先登录'
    }
    return res
  }
}

module.exports = fn

2.php发送邮件接口样板一份

我的发送邮件使用的是swiftmailer,就在写到这里时看了下swiftmailer现在已经不更新了,新的叫symfony,喜欢折腾的可以折腾一下,我就还是用我的swiftmailer了。喜欢它的原因就是:它可以自定义发送者邮件地址及发送者的名字。

你们可以把邮件组件换成自己喜欢 的,composer require 安装一下,引入。

大致接口样板如下,后期考虑再加个验证什么的,不然别人知道了,随便发也是麻烦事。

<?php
require_once './vendor/autoload.php'; //引入swiftmailer
define('SMTP_USER', 'yourname@yourserver.com'); //smtp 用户名
define('SMTP_PASSWORD', '123456'); //smtp 用户密码
define('SMTP_HOST', '127.0.0.1'); //smtp 地址或域名
define('SMTP_PORT', 25); //smtp端口,我这里就是当前主机,直接用25下面关闭了安全验证省事
define('SMTP_TLS', false); //是否启用TLS安全验证,开启为true

logRequest("Received request", $_SERVER['REQUEST_METHOD']);
// 检查是否是POST请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 获取JSON格式的请求体
    $postData = json_decode(file_get_contents('php://input'), true);

    // 验证必要的字段
    if (!isset($postData['from'], $postData['to'], $postData['subject'], $postData['html'])) {
        http_response_code(400); // 错误的请求
        echo json_encode(['success' => false, 'message' => '缺少必要的邮件参数']);
        exit;
    }

    // 邮件参数
    $from = parseFromField($postData['from']);
    $to = $postData['to'];
    $subject = $postData['subject'];
    $html = $postData['html'];

    // 发送邮件
    if (sendEmail($to, $subject, $html, $from)) {
        // 发送成功
        http_response_code(200);
        echo json_encode(['success' => true, 'message' => '邮件发送成功']);
    } else {
        // 发送失败
        http_response_code(500);
        echo json_encode(['success' => false, 'message' => '邮件发送失败']);
    }
} else {
    // 非POST请求
    http_response_code(405); // 方法不被允许
    echo json_encode(['success' => false, 'message' => '只允许POST请求']);
}

function sendEmail($to, $subject, $html, $from) {
    $loginTime = date('Y-m-d H:i:s');

    try {
        $transport = (new Swift_SmtpTransport(SMTP_HOST, SMTP_PORT,  SMTP_TLS ? 'tls' : null))
                      ->setUsername(SMTP_USER)
                      ->setPassword(SMTP_PASSWORD);
        $mailer = new Swift_Mailer($transport);

        $message = (new Swift_Message($subject))
                    ->setFrom($from)
                    ->setTo([$to => '尊敬的用户'])
                    ->setBody($html);

        $result = $mailer->send($message);
        if (!$result) {
            logRequest("邮件发送失败,没有邮件被发送。");
            return false;
        }
        return true;

    } catch (Exception $e) {
        // 记录异常信息
        logRequest("邮件发送异常: " . $e->getMessage());
        // 根据需要处理异常,例如返回false表示发送失败
        return false;
    }
}

// 解析From字段的函数
function parseFromField($from) {
    // 检查是否包含电子邮件地址和名称
    if (preg_match('/<([^>]+)>/', $from, $matches)) {
        $email = trim($matches[1], ' <>');
        $name = str_replace(['<', '>', $email], '', $from);
        return [$email => trim($name)];
    }
    // 如果没有名称,只返回电子邮件地址
    return $from;
}


function logRequest($logMessage, $method = null) { //只记录请求方式
    $logFile = 'request_log.txt';
    $timestamp = date('Y-m-d H:i:s');
    $method = $method ?? $_SERVER['REQUEST_METHOD'];
    $logEntry = "[{$timestamp}] {$method} {$logMessage}\n";
    file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}

function logRequestWithData($logMessage, $method = null) { //记录请求方式或数据
    //file_get_contents('php://input') 只能在请求被发送一次后读取,如果你在函数中多次调用它,第二次调用可能不会返回任何内容。因此,如果你需要记录POST数据,确保在请求处理的早期就进行记录。
    $logFile = 'request_log.txt';
    $timestamp = date('Y-m-d H:i:s');
    $method = $method ?? $_SERVER['REQUEST_METHOD'];
    $url = $_SERVER['REQUEST_URI'];
    $serverParams = print_r($_SERVER, true); // 打印所有服务器参数
    $postData = file_get_contents('php://input'); // 获取POST请求数据

    // 格式化日志条目,包括请求方法、URL、服务器参数和POST数据
    $logEntry = "[{$timestamp}] {$method} {$url}\n";
    $logEntry .= "Server Params: " . $serverParams . "\n";
    $logEntry .= "POST Data: " . $postData . "\n";
    $logEntry .= "Log Message: " . $logMessage . "\n";

    // 将日志条目写入文件
    file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}

?>

有图为证

 

“您的支持是我持续分享的动力”

微信收款码
微信
支付宝收款码
支付宝

目录