WordPress 自带了一个当有人评论时发邮件通知管理员的功能,但是没有一个游客的评论被管理回复时通知游客的功能,正好好奇 WordPress 的邮件发送功能是怎么实现的,于是昨天下午就在折腾这个东西,中间由于一些插曲,于是现在才弄好,记录一下。
首先尝试的思路是去找插件,找到一个很久以前的插件,WordPress 官网都已经提示两年没有更新了,估计跟这个版本很难兼容,于是开始自己动手丰衣足食,这样才能学到东西嘛。
找了一下找到这样的一段代码,文章说放到 functions.php 可以生效
/* 邮件通知 by Qiqiboy */ function comment_mail_notify($comment_id) { $comment = get_comment($comment_id);//根据id获取这条评论相关数据 $content=$comment->comment_content; //对评论内容进行匹配 $match_count=preg_match_all('/<a href="#comment-([0-9]+)?" rel="nofollow">/si',$content,$matchs); if($match_count>0){//如果匹配到了 foreach($matchs[1] as $parent_id){//对每个子匹配都进行邮件发送操作 SimPaled_send_email($parent_id,$comment); } }elseif($comment->comment_parent!='0'){//以防万一,有人故意删了@回复,还可以通过查找父级评论id来确定邮件发送对象 $parent_id=$comment->comment_parent; SimPaled_send_email($parent_id,$comment); }else return; } add_action('comment_post', 'comment_mail_notify'); function SimPaled_send_email($parent_id,$comment){//发送邮件的函数 by Qiqiboy.com $admin_email = get_bloginfo ('admin_email');//管理员邮箱 $parent_comment=get_comment($parent_id);//获取被回复人(或叫父级评论)相关信息 $author_email=$comment->comment_author_email;//评论人邮箱 $to = trim($parent_comment->comment_author_email);//被回复人邮箱 $spam_confirmed = $comment->comment_approved; if ($spam_confirmed != 'spam' && $to != $admin_email && $to != $author_email) { $wp_email = 'wp@' . preg_replace('#^www.#', '', strtolower($_SERVER['SERVER_NAME'])); // e-mail 發出點, no-reply 可改為可用的 e-mail. $subject = '您在 [' . get_option("blogname") . '] 的留言有了回应'; $message = '<div style="background-color:#eef2fa;border:1px solid #d8e3e8;color:#111;padding:0 15px;-moz-border-radius:5px;-webkit-border-radius:5px;-khtml-border-radius:5px;"> <p>' . trim(get_comment($parent_id)->comment_author) . ', 您好!</p> <p>您曾在《' . get_the_title($comment->comment_post_ID) . '》的留言:<br />' . trim(get_comment($parent_id)->comment_content) . '</p> <p>' . trim($comment->comment_author) . ' 给你的回复:<br />' . trim($comment->comment_content) . '<br /></p> <p>您可以点击 <a href="' . htmlspecialchars(get_comment_link($parent_id,array("type" => "all"))) . '">查看回复的完整內容</a></p> <p>欢迎再度光临 <a href="' . get_option('home') . '">' . get_option('blogname') . '</a></p> <p>(此邮件由系统自动发出, 请勿回复.)</p></div>'; $from = "From: "" . get_option('blogname') . "" <$wp_email>"; $headers = "$fromnContent-Type: text/html; charset=" . get_option('blog_charset') . "n"; // 从全局域获得phpmailer对象 global $phpmailer; // Make sure the PHPMailer class has been instantiated // (copied verbatim from wp-includes/pluggable.php) // (Re)create it, if it's gone missing if ( !is_object( $phpmailer ) || !is_a( $phpmailer, 'PHPMailer' ) ) { require_once ABSPATH . WPINC . '/class-phpmailer.php'; require_once ABSPATH . WPINC . '/class-smtp.php'; $phpmailer = new PHPMailer(); } // Set SMTPDebug to level 2 $phpmailer->SMTPDebug = 2; // Start output buffering to grab smtp debugging output ob_start(); $send_result = wp_mail( $to, $subject, $message, $headers ); ob_get_clean(); //var_dump($send_result); //var_dump($phpmailer); } }
于是就找到 functions.php 放进去,但是立刻就报错了,重新排查了一下,发现有两个 functions.php 文件,而正确的做法是要放到主题的文件下的 functions.php 文件,于是移过去,再试,还错,于是开始纠结,回去看代码。
这个函数的思路很简单,就是找评论的父节点,然后取得其 email 地址,拼接字符串,调用 wp_mail 函数来发送,最后再使用全局的钩子,把这个函数 hiook 到评论的时候触发。
看来重点在 wp_mail 函数,跟进去看看
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) { // Compact the input, apply the filters, and extract them back out extract( apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) ) ); if ( !is_array($attachments) ) $attachments = explode( "n", str_replace( "rn", "n", $attachments ) ); global $phpmailer; // (Re)create it, if it's gone missing if ( !is_object( $phpmailer ) || !is_a( $phpmailer, 'PHPMailer' ) ) { require_once ABSPATH . WPINC . '/class-phpmailer.php'; require_once ABSPATH . WPINC . '/class-smtp.php'; $phpmailer = new PHPMailer( true ); } // Headers if ( empty( $headers ) ) { $headers = array(); } else { if ( !is_array( $headers ) ) { // Explode the headers out, so this function can take both // string headers and an array of headers. $tempheaders = explode( "n", str_replace( "rn", "n", $headers ) ); } else { $tempheaders = $headers; } $headers = array(); $cc = array(); $bcc = array(); // If it's actually got contents if ( !empty( $tempheaders ) ) { // Iterate through the raw headers foreach ( (array) $tempheaders as $header ) { if ( strpos($header, ':') === false ) { if ( false !== stripos( $header, 'boundary=' ) ) { $parts = preg_split('/boundary=/i', trim( $header ) ); $boundary = trim( str_replace( array( "'", '"' ), '', $parts[1] ) ); } continue; } // Explode them out list( $name, $content ) = explode( ':', trim( $header ), 2 ); // Cleanup crew $name = trim( $name ); $content = trim( $content ); switch ( strtolower( $name ) ) { // Mainly for legacy -- process a From: header if it's there case 'from': if ( strpos($content, '<' ) !== false ) { // So... making my life hard again? $from_name = substr( $content, 0, strpos( $content, '<' ) - 1 ); $from_name = str_replace( '"', '', $from_name ); $from_name = trim( $from_name ); $from_email = substr( $content, strpos( $content, '<' ) + 1 ); $from_email = str_replace( '>', '', $from_email ); $from_email = trim( $from_email ); } else { $from_email = trim( $content ); } break; case 'content-type': if ( strpos( $content, ';' ) !== false ) { list( $type, $charset ) = explode( ';', $content ); $content_type = trim( $type ); if ( false !== stripos( $charset, 'charset=' ) ) { $charset = trim( str_replace( array( 'charset=', '"' ), '', $charset ) ); } elseif ( false !== stripos( $charset, 'boundary=' ) ) { $boundary = trim( str_replace( array( 'BOUNDARY=', 'boundary=', '"' ), '', $charset ) ); $charset = ''; } } else { $content_type = trim( $content ); } break; case 'cc': $cc = array_merge( (array) $cc, explode( ',', $content ) ); break; case 'bcc': $bcc = array_merge( (array) $bcc, explode( ',', $content ) ); break; default: // Add it to our grand headers array $headers[trim( $name )] = trim( $content ); break; } } } } // Empty out the values that may be set $phpmailer->ClearAddresses(); $phpmailer->ClearAllRecipients(); $phpmailer->ClearAttachments(); $phpmailer->ClearBCCs(); $phpmailer->ClearCCs(); $phpmailer->ClearCustomHeaders(); $phpmailer->ClearReplyTos(); // From email and name // If we don't have a name from the input headers if ( !isset( $from_name ) ) $from_name = 'WordPress'; /* If we don't have an email from the input headers default to wordpress@$sitename * Some hosts will block outgoing mail from this address if it doesn't exist but * there's no easy alternative. Defaulting to admin_email might appear to be another * option but some hosts may refuse to relay mail from an unknown domain. See * http://trac.wordpress.org/ticket/5007. */ if ( !isset( $from_email ) ) { // Get the site domain and get rid of www. $sitename = strtolower( $_SERVER['SERVER_NAME'] ); if ( substr( $sitename, 0, 4 ) == 'www.' ) { $sitename = substr( $sitename, 4 ); } $from_email = 'wordpress@' . $sitename; } // Plugin authors can override the potentially troublesome default $phpmailer->From = apply_filters( 'wp_mail_from' , $from_email ); $phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name ); // Set destination addresses if ( !is_array( $to ) ) $to = explode( ',', $to ); foreach ( (array) $to as $recipient ) { try { // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>" $recipient_name = ''; if( preg_match( '/(.+)s?<(.+)>/', $recipient, $matches ) ) { if ( count( $matches ) == 3 ) { $recipient_name = $matches[1]; $recipient = $matches[2]; } } $phpmailer->AddAddress( trim( $recipient ), $recipient_name); } catch ( phpmailerException $e ) { continue; } } // Set mail's subject and body $phpmailer->Subject = $subject; $phpmailer->Body = $message; // Add any CC and BCC recipients if ( !empty( $cc ) ) { foreach ( (array) $cc as $recipient ) { try { // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>" $recipient_name = ''; if( preg_match( '/(.+)s?<(.+)>/', $recipient, $matches ) ) { if ( count( $matches ) == 3 ) { $recipient_name = $matches[1]; $recipient = $matches[2]; } } $phpmailer->AddCc( trim($recipient), $recipient_name ); } catch ( phpmailerException $e ) { continue; } } } if ( !empty( $bcc ) ) { foreach ( (array) $bcc as $recipient) { try { // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>" $recipient_name = ''; if( preg_match( '/(.+)s?<(.+)>/', $recipient, $matches ) ) { if ( count( $matches ) == 3 ) { $recipient_name = $matches[1]; $recipient = $matches[2]; } } $phpmailer->AddBcc( trim($recipient), $recipient_name ); } catch ( phpmailerException $e ) { continue; } } } // Set to use PHP's mail() $phpmailer->IsMail(); // Set Content-Type and charset // If we don't have a content-type from the input headers if ( !isset( $content_type ) ) $content_type = 'text/plain'; $content_type = apply_filters( 'wp_mail_content_type', $content_type ); $phpmailer->ContentType = $content_type; // Set whether it's plaintext, depending on $content_type if ( 'text/html' == $content_type ) $phpmailer->IsHTML( true ); // If we don't have a charset from the input headers if ( !isset( $charset ) ) $charset = get_bloginfo( 'charset' ); // Set the content-type and charset $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset ); // Set custom headers if ( !empty( $headers ) ) { foreach( (array) $headers as $name => $content ) { $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) ); } if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) ) $phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;nt boundary="%s"", $content_type, $boundary ) ); } if ( !empty( $attachments ) ) { foreach ( $attachments as $attachment ) { try { $phpmailer->AddAttachment($attachment); } catch ( phpmailerException $e ) { continue; } } } do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) ); // Send! try { $phpmailer->Send(); } catch ( phpmailerException $e ) { return false; } return true; }
这个函数做的事情其实比较基本,就是根据 SMTP 协议来准备字符串,处理附件之类的事情。
这里需要温习一下 SMTP 协议,之前安装 WordPress 的时候也说过,想不通邮件的发送机理,于是最近就好好的读了一下 TCP/IP 协议族的东西,现在理论上基本理解了,其实 SMTP 无非就是两个邮箱服务器之间基于 TCP 流上的字符串交互,从 TCP 的层面看,跟 HTTP 没有本质区别,而这些 TCP 也好, UDP 也好的数据包,从 IP 层面也是没有区别的,本质都是一个要送达互联网另一端主机的数据包。
SMTP 协议的交互是类似 FTP , HTTP 那样的,发起方发送一条开始通信的字符串,接收方开始回应,指示下一步动作,例如要求发送者邮箱,要求回送邮箱,要求正文等等,发送方就依次按照要求,把这些数据一一交付,接收方把这些数据投放到对应的用户的邮箱,然后回应成功或者用户没找到之类的,就这么结束。
根据维基百科,一次典型的交互类似这样
S: 220 www.example.com ESMTP Postfix C: HELO mydomain.com S: 250 Hello mydomain.com C: MAIL FROM: <sender@mydomain.com> S: 250 Ok C: RCPT TO: <friend@example.com> S: 250 Ok C: DATA S: 354 End data with <CR><LF>.<CR><LF> C: Subject: test message C: From:""< sender@mydomain.com> C: To:""< friend@example.com> C: C: Hello, C: This is a test. C: Goodbye. C: . S: 250 Ok: queued as 12345 C: quit S: 221 Bye
可以看到也是类似 HTTP 那样用 200 , 250 ,之类的数码来表示状态的。
至于附件,这个 HTTP 也比较类似,一开始的时候是没有考虑附件的,后来有这个需求的时候,面临的问题就是怎么兼容,让字符形式可以传递二进制的文件,解决的方案是使用 Base64 编码,顾名思义,Base64 就是一种使用 a-z A-z 0-9 共 26 + 26 + 10 = 62 加上另外两个其他的可打印字符来编码二进制文件的方法,这样的目的就是可以用字符来承载二进制,进行传输,进一步的了解可以看维基百科,http://zh.wikipedia.org/wiki/B… ,有意思的是,SMTP 的文本附件混杂也是像 HTTP 那样用 boundary 来分割的。这点在上面的函数中也有体现。
现在再来回头看前面的函数就比较好理解了,无非就是准备一些交流的 SMTP 内容,跟对方服务器交互,最后看到调用了 phpmailer 对象的方法把东西发出去,这个的内容比较长,就不贴出来了,进去是一个封装的很好的发送类,基本上只要关心接口就可以。
另外,由于 SAE 不支持 php 的 mail 函数直接发送内容,于是只能通过第三方的 SMTP 服务,搜了一下发现腾讯有域名邮箱服务,另外还有一个企业邮箱服务,相同点是都可以使用自己的域名,但是据说企业邮箱的功能强大些,于是出于测试目的,先注册企业邮箱,基本的工作原理是我们需要在 DNS 解析服务器那里,把邮件的 MX 类型的 DNS 请求解析到腾讯的邮箱服务器,也就是说,所有发往我们自定义域名的邮件,都会送到腾讯的服务器,于是去改 DNS 记录,改好了,在 SMTP 的插件中填好用户名密码,开始调试。
那么基本理解了 SMTP 的原理之后,要怎么查问题呢,回去看 SAE 上面的 SMTP 插件,里面有一个发送测试邮件的方法,里面会详尽的输出 debug 信息,写法类似这样,
// Set up the mail variables $to = $_POST['to']; $subject = 'WP Mail SMTP: ' . __('Test mail to ', 'wp_mail_smtp') . $to; $message = __('This is a test email generated by the WP Mail SMTP WordPress plugin.', 'wp_mail_smtp'); // Set SMTPDebug to level 2 $phpmailer->SMTPDebug = 2; // Start output buffering to grab smtp debugging output ob_start(); // Send the test mail $result = wp_mail($to,$subject,$message); // Strip out the language strings which confuse users //unset($phpmailer->language); // This property became protected in WP 3.2 // Grab the smtp debugging output $smtp_debug = ob_get_clean(); // Output the response
于是照猫画虎,搬到发送的函数里面,终于从服务器的回应中看到,回复邮箱出错,看回代码,我们自己的邮箱是 wp 开头的,但是回复的邮箱却是写的 no-reply 开头的,这样就被服务器拒接了,于是把 no-reply 改成自己的邮箱名,就成功的发出了邮件。
昨天下午就到了这个步骤,但是想到注销企业邮箱试试域名邮箱的时候,却发现一直提示说“域名出于24小时保护期内”,不知道为什么要设这个限制,难道是怕 DNS 解析错乱吗,不管怎么用,知道今天才成功的改换到域名邮箱,其实,域名邮箱跟企业邮箱的区别可以这么来说,企业邮箱就是一套完整的系统,甚至包括一些内部的公告,交流等,是实实在在会多一个邮箱可以用,而域名邮箱,本质是只是给自己的 QQ 邮箱增加一个自定义的域名而已,并没有多一个邮箱。
Pingback: 把 WordPress 搬出 SAE | ZRJ