Home >Backend Development >PHP Tutorial >Implementing an Amazon SES proxy server using PHP

Implementing an Amazon SES proxy server using PHP

WBOY
WBOYOriginal
2016-07-25 08:46:301218browse

Understanding this article requires you to have a certain foundation in using SES. If you don’t understand, you can read the discussion in this question. http://segmentfault.com/q/1010000000095210

SES’s full name is Simple Email Service, which is a basic email service launched by Amazon. As part of AWS basic services, it inherits the traditional advantages of AWS -- cheap.

Yes, it’s really cheap. This is why I don't use mailgun or any other more awesome email service. If you send 100,000 emails per month, you will basically only need to pay about ten dollars. Compared with other services that often start at hundreds of dollars, this price advantage is huge. Therefore, with this I can tolerate its many shortcomings.

But as the number of people using SES in China increased, he was suddenly blocked one day at the end of last year, which was fatal. So, I started trying to build a proxy on my own server overseas to continue using this service. At the same time, this also provides an opportunity for me to improve its API to implement some more valuable functions, such as mass mailing.

So I didn’t use an overseas server to directly make a reverse proxy to play. This only solved the superficial problem, but my need to expand functions was impossible to achieve. Therefore, I set two basic goals for designing this SES agent

Fully compatible with the original API interface, which means that the original code basically does not need to be changed to use the proxy Implement mass mailing function

Achieving the first point is actually very simple. In fact, it is to use PHP to implement a reverse proxy, receive the parameters sent, and then use the curl component to send it to the real SES server after assembly, and then directly output it to the client after obtaining the receipt. . This is a standard agency process. My code is given below, and I have commented on the important parts

It should be noted that these codes need to be placed in the root directory of the domain name. Of course, second-level domain names can also be used

  1. include __DIR__ . '/includes.php';
  2. // Here are a few more important headers, others do not need to be paid attention to
  3. $headers = array(
  4. 'Date: ' . get_header ('Date'),
  5. 'Host: ' . SES_HOST,
  6. 'X-Amzn-Authorization: ' . get_header('X-Amzn-Authorization')
  7. );
  8. // Then assemble the url again to request this correct SES server
  9. $url = 'https://' . SES_HOST . '/'
  10. . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
  11. $ch = curl_init();
  12. curl_setopt($ch, CURLOPT_USERAGENT, 'SimpleEmailService/php');
  13. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1);
  14. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
  15. // needs to be processed The only ones are the `POST` and `DELETE` methods. There are many `GET` methods so I won’t implement them one by one
  16. // In fact, they are all methods to obtain the current information. You can view this information directly in the background
  17. switch ($_SERVER ['REQUEST_METHOD']) {
  18. case 'GET':
  19. break;
  20. case 'POST':
  21. global $HTTP_RAW_POST_DATA;
  22. $data = empty($HTTP_RAW_POST_DATA) ? file_get_contents('php://input')
  23. : $ HTTP_RAW_POST_DATA;
  24. $headers[] = 'Content-Type: application/x-www-form-urlencoded';
  25. parse_data($data);
  26. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  27. curl_setopt($ch , CURLOPT_POSTFIELDS, $data);
  28. break;
  29. case 'DELETE':
  30. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
  31. break;
  32. default:
  33. break;
  34. }
  35. curl_setopt($ch, CURLOPT_HTTPHEADER, $ headers);
  36. curl_setopt($ch, CURLOPT_HEADER, false);
  37. curl_setopt($ch, CURLOPT_URL, $url);
  38. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  39. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  40. $response = curl_exec($ch);
  41. $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  42. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  43. curl_close($ch);
  44. header('Content-Type : ' . $content_type, true, $status);
  45. echo $response;
Copy code

This code is very simple, but there are some tricks that need to be paid attention to. Among them, I used a private function called parse_data when processing the POST method. This function is actually the key to implementing mass mailing.

Speaking of this, I have to mention the SES email sending API. SES only provides a simple email sending API, which supports multiple sending objects. But when you send to multiple recipients, it will also In the recipient column, you can see the addresses of other recipients. Of course, it also supports the carbon copy function of cc or bcc, but when you use this carbon copy function to send group emails, the recipients will see that they are among the carbon copy objects, not among the recipients. For a regular website, these are obviously intolerable.

So we need a real concurrent interface to send emails. You must know that the quota assigned to me by SES is to send 28 emails per second (each person has a different quota). If fully utilized, 100,000 emails can be sent per hour, which is completely It can meet the needs of medium-sized websites.

So I came up with an idea, Without changing the client interface at all, I unpacked an email with multiple recipients sent over the proxy server into multiple emails with a single recipient. emails, and then send these emails to SES using an asynchronous queue. This is what the parse_data function does. Below I will directly give the code in includes.php, which contains all the private functions to be used. Please modify the previous define according to your own needs

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

You can see that the parse_data function starts from the second recipient, assembles them into separate emails, and puts them in the redis queue for other independent processes to read and send.

Why not start with the first recipient?

Because it needs to be compatible with the original protocol, when the client sends an email request, you always have to return something to it. I am too lazy to forge it, so the email request of the first recipient is sent directly. Without entering the queue, I can get a real SES server receipt and return it to the client, and the client code can handle this return without any modification.

What should I do if SES emails require a signature?

Yes, all SES emails require a signature. So after you unpack it, the email data changes, so the signature must change too. The my_build_query function does this, it re-signs the request parameters.

The following is the last component of this proxy system, the mail sending queue implementation, which is also a php file. You can use the nohup php command to start several php processes in the background according to your own quota size to achieve concurrent mail sending. Its structure is also very simple, it is to read the emails in the queue and then use curl to send the request

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

The above is my entire idea of ​​​​writing the SES mail proxy server. Everyone is welcome to discuss it together.

Proxy server, PHP, Amazon


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn