zoukankan      html  css  js  c++  java
  • 邮件发送那点事

    在中国做网站有两个魔咒,第一个就是注册邮件总收不到

    为什么要自己做

    在中国做网站有两个魔咒你总是逃离不掉的。

    第一个就是注册邮件总有用户收不到;另外一个就是一出名立马被DDOS。

    为了避免第一个魔咒,我们早早的买了SendCloud的付费用户。

    每天几十个到几百个注册用户,自以为很靠谱。直到有一次首页改版,我们在邮件发送的提示信息页面放上了客服电话。

    然后隔三差五就有客户电话投诉说都不到激活邮件,我们很郁闷啊,我们用的可是「东半球最好的」邮件服务商。

    后来每天都手工重发邮件实在忍不了了,罗飞说,我们用WebHook做个弹回吧,一旦发送不成功,就自动给我们运营人员发一封邮件,我们直接转发就好了。

    结果我们发现有些成功发送的邮件也被Hook回来了。然后罗飞就崩溃了 T__T 。

    其实我不是来吐槽的,SendCloud依然是国内邮件服务商的首选,毕竟现在也就这么一家。

    上个周末我仔细分析了下问题,并尝试着做了解决方案,写出来和大家分享下。

    需求

    首先,我们的需求有些特殊。求职者在JobDeer是不需要注册的,直接上传简历,由顾问开通帐号;而招聘方,我们要求必须使用公司邮箱,不允许使用QQ之类的公共邮箱。

    很多公司的企业邮箱启用了IP反垃圾,而很多发垃圾的垃圾用SendCloud和Mailgun发邮件——他们默认都是共享IP的,所以很快就进入了IP黑名单。

    SendCloud的大部分客户都是往公众邮箱发信,所以只要让QQ、Gmail和163这几家把自己加到白名单就OK了。但企业邮箱这边就未必了。

    这是我们认为共享IP的通用发信平台效果差的原因。不一定对,只是我们的思考而已。

    解决方案

    那么怎么解决呢?换独立IP。

    sendcloud

    SendCloud的独立IP贵得让人肉疼。据说是因为手上的公有IP资源有限,只能限制价格。作为做过云的同学我表示理解,但是理解后还是买不起。

    于是我去看了看Mailgun的价格 —— 每个月59美刀,这个勉强能够接受。但是鉴于我们的发信量如此之小,总感觉不爽。

    最后我决定自己搭一个MTA,就是邮件传输服务器。原因如下:

    • 企业邮箱应该不会对新的域名和IP添加黑名单,我们发送的都是正规内容的注册和订阅邮件,所以不会触发黑名单规则。
    • 我们的整体发信量在每天千级别,这样的量不会触发公共邮箱的禁止规则
    • 我们在美团云有好几个有公网IP的VPS,直接搭建就好,不用额外购买IP。

    搭建Postfix

    作为患有操作系统维护恐惧症的前PHP程序员,我一般都用Ubuntu。在Ubuntu上安装Postfix是非常容易的事情。

    apt-get install 以后,有字符界面可以选择和进行设置。我选择的是 Internet with SmartHost。

    具体的安装流程可参考这里: https://www.digitalocean.com/community/tutorials/how-to-install-and-setup-postfix-on-ubuntu-14-04

    需要注意的是,Postfix默认只对本地IP的Client开放,因为我们是用PHP调用Postfix发送,所以没有修改。

    配置PHP

    在php.ini中,修改sendmail_path 为 /usr/sbin/sendmail -t -i , 这样PHP的Mail函数就可以发出正常的邮件了。

    用Mail函数直接发送会有些小麻烦,除了编码,它会把from写成 www-data@yourserverdomain.com 。没找到哪儿改,我就直接用PHPMailer发送了。

    $mail = $GLOBALS['LP_MAILER'];
    $mail->CharSet = 'UTF-8';
    $mail->Encoding = 'base64';
    $mail->MessageID = $mid . '@'.c('mail_domain');
    
    $mail->SetFrom( c('mail_from') );
    $mail->AddReplyTo( c('mail_from') );
    
    $mail->Subject = $subject ;
    $mail->WordWrap = 50;
    $mail->MsgHTML($body);
    $mail->AddAddress( $to );
    
    if(!$mail->Send())
    {
        $GLOBALS['LP_MAILER_ERROR'] = $mail->ErrorInfo;
        return false;
    }
    else
    {
        $mail->ClearAddresses();
        return true;
    }
    

    在PHPMailer中发送的时候是可以随意指定from的,不过别开心,from和实际发信用户不同时,邮件在很多系统都会被标记成垃圾邮件的。

    同时,邮件发送是一个耗时操作,不应该让web进程长时间等待。否则,稍微有点并发服务器就要挂了。怎么办?做实时队列。

    Redis队列

    别用cron来做队列,土。其实Redis从某版本开始,提供了阻塞读的Pub/Sub服务。这个东西用来做实时队列非常好用。要更好的时候这个队列,强烈建议安装phpredis的pecl扩展。

    Pub/Sub 服务的逻辑很简单。用命令行起一个PHP,订阅到一个Channel,这个PHP就一直等着。Web程序只要用Redis把数据Pub到同一个Channel里边,命令行的PHP就会获得数据并触发callback函数。

    上点代码,订阅者:

    ini_set('default_socket_timeout', -1);
    
    $redis = new Redis();
    $redis->connect('127.0.0.1',6379);
    $channelname = c('mail_channel'); 
    try
    {
        $redis->subscribe(array($channelname), 'mailsend');
    }catch(Exception $e)
    {
    echo $e->getMessage();
    }
    

    顺便说下default_socket_timeout,如果你要用PHP长期连接socket,一定要设置这个值,不然会断的。

    上边的代码会让这个PHP一直保持运行状态,不会结束,这就是为什么我推荐pecl扩展的原因,不用写while,它自己会处理,有数据的时候,会回调 mailsend函数。

    function mailsend($instance, $channelName, $message) 
    

    mailsend函数能获取以上参数,其中$message最重要。一般把数组序列化后,通过publish传递过来。 下边是发布者的代码:

    $redis = new Redis();
    $redis->connect('127.0.0.1',6379);
    
    $info = array();
    $info['to'] = $to;
    $info['subject'] = $subject;
    $info['content'] = $content;
    
    if($ret = $redis->publish( c('mail_channel') , serialize($info) ))
    {
    return send_result( 'send to ' . $to . ' add to queue' );
    }
    else 
        return send_error( $ret );
    

    很简单,用起来也非常方便。

    上边说过,因为调用mail函数的用户是www-data,所以真实的发信箱是www-data@yourserverdomain.com ,而你想显示为 easy@yourserverdomain.com 。要保证一致性其实很简单,用easy的用户启动订阅者PHP即可。

    su easy
    nohup php sub.php & 
    

    进一步适配反垃圾规则

    为了防止别人冒用你的邮箱地址给公共邮箱发信,你可以启用SPF和DKIM。

    如果只是发信,SPF不用安装什么的东西,直接在发信域名的DNS中加一条TXT记录就可以了。格式大概是这样:

    v=spf1 ip4:106.3.32.60 ~all 
    

    这句话告诉了收件服务器,这个域名下的邮箱如果不是106.3.32.60 发过来的,直接标记为垃圾。

    DKIM相对复杂一些,它会对邮件内容进行签名,然后收件服务器通过DNS获取公钥,核对签名是否正确。

    具体的操作是给Postfix添加一个内容filter。详细说明参考这里:https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-dkim-with-postfix-on-debian-wheezy

    这些都做完以后,别人很难把自己发的垃圾邮件栽赃给你了。只要洁身自好,就可以顺利的通过反垃圾规则。

    内容反垃圾

    在测试过程中,我发现自己的测试邮箱进了Gmail的垃圾箱,Gmail有个好处是它会标明为啥邮件会进垃圾箱。这对我们调整邮件内容很有用。一般来讲,尽可能把完整内容写到正文中,比光发附件要靠谱很多。还有一些垃圾邮件常用的广告词,要注意避免。

    进一步的思考

    在完成了Postfix自发邮件的功能后,我和罗飞讨论了云服务的不靠谱性。我们觉得应该把云服务视为不靠谱的,它总会有出故障的时候,包括我们自己搭建的服务。

    如何在不靠谱的云上搭建一个靠谱的业务支撑?去单点。是的,云也应该视为单点。如果我们把同一类云服务重叠起来,当第一层服务down了,自动启用第二层服务,那么因为云不稳定而影响业务的可能性就会大大降低。

    因为0.1% * 0.1% = 0.0001% ,两个服务同时出故障的可能性大大降低了。

    在这个思路下我们做了「九尾猫」。它提供一种机制,保证主服务商挂掉后会自动启用备用服务商。如同猫的九条命一样,只要服务商不全挂掉,业务本身就不会挂掉。

    九尾猫有几个核心概念

    • Service:云服务,现在只有mail一种
    • Method:服务商,现在有Mailgun和Postfix两层,以后会把SendCloud(等他们把Hook修好,买个半独立IP试试运气)、新浪邮件服务(是的,新浪也做了,正在小规模内测)和SMTP接入进来。
    • Level:优先级,每个Method分配一个优先级,当level1挂了后,会自动寻找level2进行处理,依次累加,最多level9。可以根据服务质量执行调整各个服务商的level。

    九尾猫是我们的一个小尝试,主要服务内部,写出来主要是和大家分享思路。用这个思路,在短信发送、简历分析等不可靠的服务上,均可以封装出可靠的云。我们会慢慢优化它,在合适的时候考虑对外提供服务。

  • 相关阅读:
    在Objective-C声明Block的几种方式
    属性初始化
    OC协议
    堆排序的OC实现
    iOS 应用性能测试的相关方法、工具及技巧
    墙裂推荐 iOS 资源大全
    剖析@weakify 和 @strongify
    iOS开发大神必备的Xcode插件
    聊聊 KVC 和 KVO 的高阶应用
    TableView的优化
  • 原文地址:https://www.cnblogs.com/fyblzds/p/12780234.html
Copyright © 2011-2022 走看看