用PHP实现一个Amazon SES的代理服务器

看懂这篇文章需要你有一定的SES使用基础,如果你不明白,可以看这个问题里的讨论

SES的全称是Simple Email Service,它是亚马逊公司推出的一个邮件基础服务。作为AWS基础服务的一部分,它继承了AWS的传统优势 -- 便宜

是的,真的非常便宜。这就是为什么我没用mailgun或者其它什么更牛逼邮件服务的原因。如果每月你发10万封邮件的话,基本也只需要支付十多美刀左右。这和其它那些动辄上百美刀起步的服务来说,价格优势很大。所以,凭着这个我也能忍受它的诸多缺点。

但是随着国内用SES的人增多,他在去年底的某一天突然被墙了,这可要了命了。于是,我开始尝试在境外自己的服务器上做一层代理来继续使用这个服务。同时这也提供了一个契机,让我可以有机会对它的api作出改进来实现一些更有价值的功能,比如邮件群发。

因此我没有用境外服务器直接做一个反向代理来玩,这样只是解决了表面上的问题,但我扩展功能的需求就不可能实现了。因此我为设计这个SES代理订立了两个基本目标

完全兼容原有api接口,这意味着原有代码基本不需要改变就可以用代理 实现邮件群发功能

实现第一点其实非常简单,其实就是用php实现了一个反向代理,把发送过来的参数接收到,然后组装后使用curl组件发送给真正的SES服务器,取得回执后再直接输出给客户端。这就是一个标准的代理流程,下面给出我的代码,里面重要的部分我都给出了注释

需要注意的是这些代码需要放在域名的根目录下,当然二级域名也可以

  1. <?php
  2. include __DIR__ . '/includes.php';
  3. // 这里是几个比较重要的header,其它不需要关注
  4. $headers = array(http://www.kmnk03.com/hxpfk/bpy/130.html
  5. 'Date: ' . get_header('Date'),
  6. 'Host: ' . SES_HOST,
  7. 'X-Amzn-Authorization: ' . get_header('X-Amzn-Authorization')
  8. );
  9. // 然后再次组装url以请求这正的SES服务器
  10. $url = 'https://' . SES_HOST . '/'
  11. . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
  12. $ch = curl_init();http://www.kmnk03.com/hxpfk/bpy/131.html
  13. curl_setopt($ch, CURLOPT_USERAGENT, 'SimpleEmailService/php');
  14. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1);
  15. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
  16. // 需要处理的就是`POST`和`DELETE`方法,`GET`方法比较繁多我就不一一实现了
  17. // 其实都是一些获得当前信息的方法,这些信息你可以直接到后台看
  18. switch ($_SERVER['REQUEST_METHOD']) {
  19. case 'GET':
  20. break;
  21. case 'POST':
  22. global $HTTP_RAW_POST_DATA;http://www.kmnk03.com/hxpfk/pfgm/132.html
  23. $data = empty($HTTP_RAW_POST_DATA) ? file_get_contents('php://input')
  24. : $HTTP_RAW_POST_DATA;
  25. $headers[] = 'Content-Type: application/x-www-form-urlencoded';
  26. parse_data($data);
  27. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  28. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  29. break;
  30. case 'DELETE':
  31. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
  32. break;
  33. default:
  34. break;
  35. }
  36. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  37. curl_setopt($ch, CURLOPT_HEADER, false);
  38. curl_setopt($ch, CURLOPT_URL, $url);
  39. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  40. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  41. $response = curl_exec($ch);
  42. $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  43. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  44. curl_close($ch);
  45. http://www.kmnk03.com/hxpfk/dzpz/133.html
  46. header('Content-Type: ' . $content_type, true, $status);
  47. echo $response;
复制代码

这段代码非常简单,但也有些技巧需要注意,其中我处理POST方法时使用了一个名为parse_data的私有函数,这个函数实际上是实现群发邮件的关键。

说到这里我不得不提一下SES发邮件的API,SES只提供一个简单的邮件发送API,其中它的发送对象支持多个,但当你发送给多个收件人时,它也会在收件人栏看到其他收件人的地址。当然它也支持cc或者bcc的抄送功能,但当你在使用这种抄送功能来实现群发邮件时,收件者会看到自己是在抄送对象中,而不是在接收人中。对于一个正规网站来说,这些显然是不能容忍的。

因此我们需要真正的并发接口来发送邮件,要知道SES分配给我的配额是每秒钟可以发送28封邮件(每人配额不同),要是完全利用的话每小时可以发送10万封邮件,完全可以满足中型网站的需求了。

因此我产生了一个想法,在完全不改变客户端接口的情况下,我在代理服务器上将发送过来的有多个收件人的一封邮件拆包成一个一个单个收件人的多封邮件,然后再将这些邮件用异步队列的方式发送到SES上。这就是parse_data函数所做的事情,下面我直接给出includes.php里的代码,这里包含了所有要用到的私有函数,前面的define定义请根据自己的需求修改

  1. <?php
  2. define('REDIS_HOST', '127.0.0.1');
  3. define('REDIS_PORT', 6379);http://www.kmnk03.com/hxpfk/qcd/134.html
  4. define('SES_HOST', 'email.us-east-1.amazonaws.com');
  5. define('SES_KEY', '');
  6. define('SES_SECRET', '');
  7. /**
  8. * get_header
  9. *
  10. * @param mixed $name
  11. * @access public
  12. * @return void
  13. */
  14. function get_header($name) {
  15. $name = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
  16. return isset($_SERVER[$name]) ? $_SERVER[$name] : '';
  17. }
  18. /**
  19. * my_parse_str
  20. *
  21. * @param mixed $query
  22. * @param mixed $params
  23. * @access public
  24. * @return void
  25. */http://www.kmnk03.com/hxpfk/tf/135.html
  26. function my_parse_str($query, &$params) {
  27. if (empty($query)) {
  28. return;
  29. }
  30. $decode = function ($str) {
  31. return rawurldecode(str_replace('~', '%7E', $str));
  32. };
  33. $data = explode('&', $query);
  34. $params = array();
  35. foreach ($data as $value) {
  36. list ($key, $val) = explode('=', $value, 2);
  37. if (isset($params[$key])) {
  38. if (!is_array($params[$key])) {
  39. $params[$key] = array($params[$key]);
  40. }http://www.kmnk03.com/hxpfk/gx/136.html
  41. $params[$key][] = $val;
  42. } else {
  43. $params[$key] = $decode($val);
  44. }
  45. }
  46. }
  47. /**
  48. * my_urlencode
  49. *
  50. * @param mixed $str
  51. * @access public
  52. * @return void
  53. */
  54. function my_urlencode($str) {
  55. return str_replace('%7E', '~', rawurlencode($str));
  56. }
  57. http://www.kmnk03.com/hxpfk/tf/137.html
  58. /**
  59. * my_build_query
  60. *
  61. * @param mixed $params
  62. * @access public
  63. * @return void
  64. */
  65. function my_build_query($parameters) {
  66. $params = array();
  67. foreach ($parameters as $var => $value) {
  68. if (is_array($value)) {
  69. foreach ($value as $v) {
  70. $params[] = $var.'='.my_urlencode($v);
  71. }
  72. } else {
  73. $params[] = $var.'='.my_urlencode($value);
  74. }
  75. }
  76. sort($params, SORT_STRING);
  77. return implode('&', $params);
  78. }
  79. /**
  80. * my_headers
  81. *
  82. * @param mixed $headers
  83. * @access public
  84. * @return void
  85. */
  86. function my_headers() {
  87. $date = gmdate('D, d M Y H:i:s e');
  88. $sig = base64_encode(hash_hmac('sha256', $date, SES_SECRET, true));
  89. http://www.kmnk03.com/hxpfk/bdf/138.html
  90. $headers = array();
  91. $headers[] = 'Date: ' . $date;
  92. $headers[] = 'Host: ' . SES_HOST;
  93. $auth = 'AWS3-HTTPS AWSAccessKeyId=' . SES_KEY;
  94. $auth .= ',Algorithm=HmacSHA256,Signature=' . $sig;
  95. $headers[] = 'X-Amzn-Authorization: ' . $auth;
  96. $headers[] = 'Content-Type: application/x-www-form-urlencoded';
  97. return $headers;
  98. }
  99. /**
  100. * parse_data
  101. *
  102. * @param mixed $data
  103. * @access public
  104. * @return void
  105. */
  106. function parse_data(&$data) {
  107. my_parse_str($data, $params);
  108. if (!empty($params)) {
  109. $redis = new Redis();
  110. $redis->connect(REDIS_HOST, REDIS_PORT);
  111. // 多个发送地址
  112. if (isset($params['Destination.ToAddresses.member.2'])) {
  113. $address = array();
  114. $mKey = uniqid();
  115. $i = 2;
  116. while (isset($params['Destination.ToAddresses.member.' . $i])) {
  117. $aKey = uniqid();
  118. $key = 'Destination.ToAddresses.member.' . $i;
  119. $address[$aKey] = $params[$key];
  120. unset($params[$key]);
  121. $i ++;
  122. }
  123. $data = my_build_query($params);
  124. unset($params['Destination.ToAddresses.member.1']);
  125. $redis->set('m:' . $mKey, my_build_query($params));
  126. foreach ($address as $k => $a) {
  127. $redis->hSet('a:' . $mKey, $k, $a);
  128. $redis->lPush('mail', $k . '|' . $mKey);
  129. }
  130. }
  131. }
  132. }
复制代码

可以看到parse_data函数从第二个收件人开始,把它们组装成一个一个单独的邮件,放到redis队列里,供其他独立进程读取发送。

为什么不从第一个收件人开始?

因为要兼容原有协议,客户端发过来一个发邮件请求你总要给它返回一个东西吧,我又懒得伪造,因此它的第一个收件人的发邮件请求是直接发出去了,而并没有进入队列,这样我可以取得一个真实的SES服务器回执返回给客户端,客户端代码也无需做任何修改,就可以处理这个返回。

SES的邮件都是要签名的怎么办?

是的,所有的SES邮件都需要签名。因此在你解包以后,邮件数据改变了,因此签名也必须改变。my_build_query函数就是做这个事情的,它会对请求参数做重新签名。

下面是这个代理系统的最后一个组成部分,邮件发送队列实现,它也是一个php文件,你可以根据自己的配额大小,在后台用nohup php命令启动若干个php进程,来实现并发邮件发送。它的结构也非常简单,就是读取队列里的邮件然后用curl发送请求

  1. <?php
  2. include __DIR__ . '/includes.php';
  3. $redis = new Redis();
  4. $redis->connect(REDIS_HOST, REDIS_PORT);
  5. do {
  6. $pop = $redis->brPop('mail', 10);
  7. if (empty($pop)) {
  8. continue;
  9. }http://www.kmnk03.com/hxpfk/bdf/129.html
  10. list ($k, $id) = $pop;
  11. list($aKey, $mKey) = explode('|', $id);
  12. $address = $redis->hGet('a:' . $mKey, $aKey);
  13. if (empty($address)) {
  14. continue;
  15. }
  16. $data = $redis->get('m:' . $mKey);
  17. if (empty($data)) {
  18. continue;
  19. }
  20. my_parse_str($data, $params);
  21. $params['Destination.ToAddresses.member.1'] = $address;
  22. $data = my_build_query($params);
  23. $headers = my_headers();
  24. $url = 'https://' . SES_HOST . '/';
  25. $ch = curl_init();
  26. curl_setopt($ch, CURLOPT_USERAGENT, 'SimpleEmailService/php');
  27. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  28. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  29. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  30. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  31. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  32. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  33. curl_setopt($ch, CURLOPT_URL, $url);
  34. curl_setopt($ch, CURLOPT_TIMEOUT, 10);
  35. curl_exec($ch);
  36. curl_close($ch);
  37. unset($ch);
  38. unset($data);
  39. kmnk03.com
  40. } while (true);
  41. www.kmnk03.com
复制代码

以上就是我编写SES邮件代理服务器的整个思路,欢迎大家一同来探讨。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AWS SES (Amazon Simple Email Service) 是亚马逊提供的可靠、灵活且可扩展的电子邮件发送和接收服务。它可以帮助开发者快速、高效地通过网络应用程序发送电子邮件。 在使用 PHP 进行 AWS SES 的集成时,你需要遵循以下步骤: 1. 配置 IAM 用户:首先,你需要在 AWS 管理控制台上创建一个 IAM 用户,并授予该用户适当的 SES 发送和接收权限。 2. 安装 AWS SDK for PHP:你需要在 PHP 项目中安装 AWS SDK for PHP,这是一个用于与 AWS 服务进行交互的库。你可以使用 Composer 进行安装,或者手动下载并在项目中引入。 3. 配置 AWS SES:在代码中,你需要指定 AWS SES 的凭证、区域和其他配置信息。你可以使用 IAM 用户的凭证来进行身份验证,并设置合适的区域来确保与所需的 SES 区域进行通信。 4. 发送邮件:使用 AWS SES,你可以使用 PHP 代码来发送电子邮件。通过构建合适的电子邮件消息并指定接收者、发件人、主题和正文等信息,你可以使用 `sendEmail()` 或 `sendRawEmail()` 方法来发送邮件。 AWS SES 还提供其他功能,如验证发件人邮箱、配置反垃圾邮件策略、设置电子邮件模板等等。通过使用 PHP 和 AWS SES 集成,你可以方便地在你的应用程序中实现强大的电子邮件功能。 总的来说,AWS SES 提供了一个强大的平台来发送和接收电子邮件,而PHP与AWS SES集成,可以让你更轻松地使用PHP发送电子邮件。这对于构建包括用户注册、密码重置、订单确认等功能的网站或应用程序来说,是一个非常有用的工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值