Understanding this article requires you to have a certain foundation of SES, if you do not understand, you can see the discussion of http://segmentfault.com/q/1010000000095210 in this question
SES is short for Simple Email Service, which is a Mail basic 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 didn't use mailgun or other services that are even more powerful. If you send 0.1 million emails each month, you only need to pay about 10 million USD. This is a great price advantage for other services that start with hundreds of US dollars. Therefore, I can bear many shortcomings of it. However, as the number of people using SES in China increases, he was suddenly attacked at the end of last year. As a result, I began to try to use this service as a proxy on my own servers outside China. At the same time, this also provides an opportunity for me to improve its api to implement some more valuable functions, such as sending emails. Therefore, I didn't use a foreign server to directly create a reverse proxy to play the game. this only solves the superficial problem, but I cannot implement the function expansion requirements. So I set two basic goals for designing this SES proxy Fully compatible with the original api interface, which means that the original code can be used as a proxy to implement the Mail group function without any changes.The first implementation is actually very simple. In fact, php is used to implement a reverse proxy, receive the sent parameters, and then assemble them and send them to the real SES server using the curl component, obtain the receipt and then directly output it to the client. This is a standard proxy process. my code is given below, and I have commented on all the important parts in it. Note that these codes need to be placed under the root directory of the domain name. of course, the second-level domain name can also
-
- Include _ DIR _. '/includes. php ';
- // Here are several important headers, which do not need to be followed.
- $ Headers = array (
- 'Date: '. get_header ('date '),
- 'Host: '. SES_HOST,
- 'X-Amzn-Authorization: '. get_header ('x-Amzn-authorization ')
- );
- // Then assemble the url again to request this positive SES server
- $ Url = 'https: // '. SES_HOST .'/'
- . (Empty ($ _ SERVER ['query _ string'])? '':'? '. $ _ SERVER ['query _ string']);
- $ Ch = curl_init ();
- Curl_setopt ($ ch, CURLOPT_USERAGENT, 'simpleemailservice/php ');
- Curl_setopt ($ ch, CURLOPT_SSL_VERIFYHOST, 1 );
- Curl_setopt ($ ch, CURLOPT_SSL_VERIFYPEER, 1 );
- // The 'post' and 'delete' methods need to be processed. I will not implement the 'get' methods one by one.
- // Actually, they are all methods for obtaining the current information. you can view the information directly in the background.
- Switch ($ _ SERVER ['request _ method']) {
- Case 'get ':
- Break;
- Case 'Post ':
- Global $ HTTP_RAW_POST_DATA;
- $ Data = empty ($ HTTP_RAW_POST_DATA )? File_get_contents ('php: // input ')
- : $ HTTP_RAW_POST_DATA;
- $ Headers [] = 'content-Type: application/x-www-form-urlencoded ';
- Parse_data ($ data );
- Curl_setopt ($ ch, CURLOPT_CUSTOMREQUEST, 'post ');
- Curl_setopt ($ ch, CURLOPT_POSTFIELDS, $ data );
- Break;
- Case 'delete ':
- Curl_setopt ($ ch, CURLOPT_CUSTOMREQUEST, 'delete ');
- Break;
- Default:
- Break;
- }
- Curl_setopt ($ ch, CURLOPT_HTTPHEADER, $ headers );
- Curl_setopt ($ ch, CURLOPT_HEADER, false );
- Curl_setopt ($ ch, CURLOPT_URL, $ url );
- Curl_setopt ($ ch, CURLOPT_RETURNTRANSFER, true );
- Curl_setopt ($ ch, CURLOPT_FOLLOWLOCATION, true );
- $ Response = curl_exec ($ ch );
- $ Content_type = curl_getinfo ($ ch, CURLINFO_CONTENT_TYPE );
- $ Status = curl_getinfo ($ ch, CURLINFO_HTTP_CODE );
- Curl_close ($ ch );
- Header ('content-Type: '. $ content_type, true, $ status );
- Echo $ response;
This code is very simple, but some tips need to be noted. I used a private function named parse_data when processing the POST method. This function is actually the key to implementing mass mail. Speaking of this, I have to mention the SES mail API. SES only provides a simple mail sending API, which supports multiple sending objects, but when you send to multiple recipients, it also displays the addresses of other recipients in the recipient column. Of course, it also supports cc or bcc cc, but when you use this cc function to send mass mails, the recipient will see that they are in the cc object, not in the recipient. For a regular website, these are obviously intolerable. Therefore, we need a real concurrent interface to send emails. we need to know that the quota assigned to me by SES is that 28 emails can be sent per second (different quotas per person ), if it is fully utilized, you can send 0.1 million emails per hour to meet the needs of medium-sized websites. So I came up with an idea,Without changing the client interface at all, I split an email from multiple recipients into multiple emails from one single recipient on the proxy server, then, these emails are sent to SES in an asynchronous queue.. This is what the parse_data function does. next I will give the code in mongodes. php directly. This includes all the private functions to be used. please modify the previous define definition according to your needs.
-
- Define ('redis _ host', '2017. 0.0.1 ');
- Define ('redis _ port', 6379 );
- Define ('ses _ host', 'Email .us-east-1.amazonaws.com ');
- Define ('ses _ key ','');
- Define ('ses _ secret ','');
- /**
- * Get_header
- *
- * @ Param mixed $ name
- * @ Access public
- * @ Return void
- */
- Function get_header ($ name ){
- $ Name = 'http _ '. strtoupper (str_replace ('-',' _ ', $ name ));
- Return isset ($ _ SERVER [$ name])? $ _ SERVER [$ name]: '';
- }
- /**
- * My_parse_str
- *
- * @ Param mixed $ query
- * @ Param mixed $ params
- * @ Access public
- * @ Return void
- */
- Function my_parse_str ($ query, & $ params ){
- If (empty ($ query )){
- Return;
- }
- $ Decode = function ($ str ){
- Return rawurldecode (str_replace ('~ ',' % 7e', $ str ));
- };
- $ Data = explode ('&', $ query );
- $ Params = array ();
- Foreach ($ data as $ value ){
- List ($ key, $ val) = explode ('=', $ value, 2 );
- If (isset ($ params [$ key]) {
- If (! Is_array ($ params [$ key]) {
- $ Params [$ key] = array ($ params [$ key]);
- }
- $ Params [$ key] [] = $ val;
- } Else {
- $ Params [$ key] = $ decode ($ val );
- }
- }
- }
- /**
- * My_urlencode
- *
- * @ Param mixed $ str
- * @ Access public
- * @ Return void
- */
- Function my_urlencode ($ str ){
- Return str_replace ('% 7e ','~ ', Rawurlencode ($ str ));
- }
- /**
- * My_build_query
- *
- * @ Param mixed $ params
- * @ Access public
- * @ Return void
- */
- Function my_build_query ($ parameters ){
- $ Params = array ();
- Foreach ($ parameters as $ var => $ value ){
- If (is_array ($ value )){
- Foreach ($ value as $ v ){
- $ Params [] = $ var. '='. my_urlencode ($ v );
- }
- } Else {
- $ Params [] = $ var. '='. my_urlencode ($ value );
- }
- }
- Sort ($ params, SORT_STRING );
- Return implode ('&', $ params );
- }
- /**
- * My_headers
- *
- * @ Param mixed $ headers
- * @ Access public
- * @ Return void
- */
- Function my_headers (){
- $ Date = gmdate ('d, d m y h: I: s e ');
- $ Sig = base64_encode (hash_hmac ('sha256 ', $ date, SES_SECRET, true ));
- $ Headers = array ();
- $ Headers [] = 'date: '. $ Date;
- $ Headers [] = 'host: '. SES_HOST;
- $ Auth = 'aws3-HTTPS AWSAccessKeyId = '. SES_KEY;
- $ Auth. = ', Algorithm = HmacSHA256, Signature ='. $ sig;
- $ Headers [] = 'x-Amzn-Authorization: '. $ auth;
- $ Headers [] = 'content-Type: application/x-www-form-urlencoded ';
- Return $ headers;
- }
- /**
- * Parse_data
- *
- * @ Param mixed $ data
- * @ Access public
- * @ Return void
- */
- Function parse_data (& $ data ){
- My_parse_str ($ data, $ params );
- If (! Empty ($ params )){
- $ Redis = new Redis ();
- $ Redis-> connect (REDIS_HOST, REDIS_PORT );
- // Multiple sending addresses
- If (isset ($ params ['Destination. ToAddresses. member.2 ']) {
- $ Address = array ();
- $ MKey = uniqid ();
- $ I = 2;
- While (isset ($ params ['Destination. ToAddresses. member. '. $ I]) {
- $ AKey = uniqid ();
- $ Key = 'Destination. ToAddresses. member. '. $ I;
- $ Address [$ aKey] = $ params [$ key];
- Unset ($ params [$ key]);
- $ I ++;
- }
- $ Data = my_build_query ($ params );
- Unset ($ params ['Destination. ToAddresses. member.1 ']);
- $ Redis-> set ('M: '. $ mKey, my_build_query ($ params ));
- Foreach ($ address as $ k => $ ){
- $ Redis-> hSet ('A: '. $ mKey, $ k, $ );
- $ Redis-> lPush ('mail', $ k. '|'. $ mKey );
- }
- }
- }
- }
We can see that the parse_data function starts from the second recipient and assembles them into a separate email and puts them in the redis queue for reading and sending by other independent processes. Why not start with the first recipient? To be compatible with the original protocol, the client sends an email request and you always need to return something to it. I am too lazy to forge it. Therefore, the first recipient's email request is sent directly, but it does not enter the queue, so that I can get a real SES server receipt and return it to the client, and the client code can process the return without any modifications. What should I do if all SES emails require signatures? Yes. all SES emails require signatures. Therefore, after you unpack the package, the email data is changed, so the signature must also be changed. The my_build_query function is used to re-sign the request parameters. The following is the last part of the proxy system, which is implemented by the Mail sending queue. it is also a php file. you can set the size according to your quota, start several php processes using the nohup php command in the background to implement and send emails. Its structure is also very simple, that is, reading emails in the queue and then sending requests with curl
-
- Include _ DIR _. '/includes. php ';
- $ Redis = new Redis ();
- $ Redis-> connect (REDIS_HOST, REDIS_PORT );
- Do {
- $ Pop = $ redis-> brPop ('mail', 10 );
- If (empty ($ pop )){
- Continue;
- }
- List ($ k, $ id) = $ pop;
- List ($ aKey, $ mKey) = explode ('|', $ id );
- $ Address = $ redis-> hGet ('A: '. $ mKey, $ aKey );
- If (empty ($ address )){
- Continue;
- }
- $ Data = $ redis-> get ('M: '. $ mKey );
- If (empty ($ data )){
- Continue;
- }
- My_parse_str ($ data, $ params );
- $ Params ['Destination. ToAddresses. member.1 '] = $ address;
- $ Data = my_build_query ($ params );
- $ Headers = my_headers ();
- $ Url = 'https: // '. SES_HOST .'/';
- $ Ch = curl_init ();
- Curl_setopt ($ ch, CURLOPT_USERAGENT, 'simpleemailservice/php ');
- Curl_setopt ($ ch, CURLOPT_SSL_VERIFYHOST, false );
- Curl_setopt ($ ch, CURLOPT_SSL_VERIFYPEER, false );
- Curl_setopt ($ ch, CURLOPT_CUSTOMREQUEST, 'post ');
- Curl_setopt ($ ch, CURLOPT_RETURNTRANSFER, true );
- Curl_setopt ($ ch, CURLOPT_POSTFIELDS, $ data );
- Curl_setopt ($ ch, CURLOPT_HTTPHEADER, $ headers );
- Curl_setopt ($ ch, CURLOPT_URL, $ url );
- Curl_setopt ($ ch, CURLOPT_TIMEOUT, 10 );
- Curl_exec ($ ch );
- Curl_close ($ ch );
- Unset ($ ch );
- Unset ($ data );
- } While (true );
The above is the overall idea of writing the SES mail proxy server. You are welcome to discuss it together. |