What to do with the result of AWS SES SesClient sendEmail() method? - php

Short:
What do I have to do with the result of the sendEmail() method of the SES-API from Amazon AWS?
Long:
I have successfully installed the "aws/aws-sdk-php": "^3.38" via composer in a PHP project.
I have successfully sent emails over the formula:
$client = new SesClient( $sesParameters );
$result = $client->sendEmail( $emailSesArgs );
It works.
I receive a result like this one:
Result {#433 ▼
-data: array:2 [▼
"MessageId" => "0102015fd3c21fd2-98a104e2-0c3f-4078-90ed-0be3a12ae812-000000"
"#metadata" => array:4 [▼
"statusCode" => 200
"effectiveUri" => "https://email.eu-west-1.amazonaws.com"
"headers" => array:4 [▼
"x-amzn-requestid" => "e27b7805-cd11-11e7-9d57-cd9600d88c96"
"content-type" => "text/xml"
"content-length" => "326"
"date" => "Sun, 19 Nov 2017 10:10:35 GMT"
]
"transferStats" => array:1 [▼
"http" => array:1 [▼
0 => []
]
]
]
]
}
Questions
The questions are...
What should I do with this result, further than exploring the 200 OK result in real time?
What operations can I perform afterwards using this MessageId?
I've observed that if I send an email to an invalid address, this also returns 200 OK. Probably this is more an "acknowledge" that the send-email "request" has been submitted than actually processing of it. Can I use the result to further read the "status" of the deilvey itself via API to discover if the email was successfully delivered?
Thanks!

What I do is I track message deliveries, bounces and complaints using the message ID by configuring SNS topics (SES > Domains > example.com > Notifications) that trigger an AWS Lambda function (SNS > Topics > Subscriptions), which in turn stores/updates the delivery status in a DynamoDB table for later query operations.

Related

Symfony 6 | Consume streamed file and send it back to the client

I am developing an administrator website.
There is a section that, when a button is pressed, sends a request to my Symfony backend and the Symfony backend makes an HTTP request to another API that responds with a raw Excel file.
The flow Im trying to make is:
Admin panel [http request] -> Symfony Backend [http request] -> Another API [Generate file and streams it back]
Another API [Streamed Excel file] -> Symfony Backend [Consume the stream] -> Admin panel [Save in device]
I tried doing it like this:
ReportsService
public function reserves_report(String $date1, String $date2): FileForwardResponse {
// This calls to the API to get the streamed file
$guestTypesHttpResponse = $this->httpClient->request(
'GET',
$this->get_endpoint('reports') . "/" . $date1 . "/" . $date2
);
// This class is just to organize things
return new FileForwardResponse(
$guestTypesHttpResponse->getHeaders(),
$guestTypesHttpResponse->getContent(),
);
}
Controller
public function reserves_report(): Response
{
$this->sessionService->singUserIn();
$fileResponse = $this->reportsService->reserves_report($date1, $date2);
return new Response(
$fileResponse->content,
200,
$fileResponse->headers,
);
}
What I was expecting to do is to recive the entire streamed file from the API to the Symfony backend and then use the headers from the API to make it available to download from the client since the headers looks like this:
+headers: array:12 [▶
"connection" => array:1 [▶
0 => "Keep-Alive"
]
"keep-alive" => array:1 [▶
0 => "timeout=5, max=100"
]
"x-ratelimit-limit" => array:1 [▶
0 => "600"
]
"x-ratelimit-remaining" => array:1 [▶
0 => "596"
]
"content-type" => array:1 [▶
0 => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=UTF-8"
]
"content-disposition" => array:1 [▶
0 => "attachment; filename="filename.xlsx""
]
"expires" => array:1 [▶
0 => "Mon, 26 Jul 1997 05:00:00 GMT"
]
"last-modified" => array:1 [▶
0 => "Sat, 11 Feb 2023 15:51:36"
]
"cache-control" => array:1 [▶
0 => "cache, must-revalidate"
]
"pragma" => array:1 [▶
0 => "public"
]
"transfer-encoding" => array:1 [▶
0 => "chunked"
]
"date" => array:1 [▶
0 => "Sat, 11 Feb 2023 21:51:36 GMT"
]
]
And the response content looks like this:
What I got when I hit my Symfony endpoint is a 0 bytes file named filename.
If I hit the APIs endpoint from the browser directly it downloads it just right but I would like to know if there is any way to do it through Symfony http request.

Xero PHP Oauth in Laravel throws 401 AuthenticationUnsuccessful when updating contacts

I am trying to update Xero contacts, which is part of the invoice in Laravel. I am using Xero-php-oauth2. I have tried
$xero = resolve(AccountingApi::class);
$invoice = new Invoice();
$invoice->setLineItems($lineItems);
$result = $xero->updateInvoice($tenantId, $invoice_id, $invoice);
$contactId = $result->getInvoices()[0]['contact']['contact_id'];
$contact = $this->newContact();
$contact->setContactId($contactId);
//$tenantId, $contactid, and $contact are correct
$xero->updateContact($tenantId, $contactId, $contact);
The request I am trying to send is like following, but the response returns 401 AuthenticationUnsuccessful
Request {#1160 ▼
-method: "POST"
-requestTarget: null
-uri: Uri {#1212 ▼
-scheme: "https"
-userInfo: ""
-host: "api.xero.com"
-port: null
-path: "/api.xro/2.0/Contacts/134ab308-7d07-4c2d-a770-c01325947ede"
-query: ""
-fragment: ""
}
-headers: array:6 [▼
"Host" => array:1 [▼
0 => "api.xero.com"
]
"User-Agent" => array:1 [▼
0 => "[xero-php-oauth2 (2.11.0)]"
]
"xero-tenant-id" => array:1 [▼
0 => "TENANT_ID_HERE"
]
"Accept" => array:1 [▼
0 => "application/json"
]
"Content-Type" => array:1 [▼
0 => "application/json"
]
"Authorization" => array:1 [▼
0 => "Bearer BEARER_TOKEN_HERE"
]
]
-headerNames: array:6 [▶]
-protocol: "1.1"
-stream: Stream {#1217 ▼
-stream: stream resource #755 ▼
wrapper_type: "PHP"
stream_type: "TEMP"
mode: "w+b"
unread_bytes: 0
seekable: true
uri: "php://temp"
options: []
}
-size: null
-seekable: true
-readable: true
-writable: true
-uri: "php://temp"
-customMetadata: []
}
}
xero-php-oauth2 sends request along with following options to GuzzleHttp\Client, and It seems to throw error at AccountingAPI#updateContactWithHttpInfo() in following line
$response = $this->client->send($request, $options);`
I also tried to add accounting.contacts scope before calling but it does not work.
$options = ['scope' => ['openid email profile offline_access accounting.settings accounting.transactions accounting.contacts accounting.journals.read accounting.reports.read accounting.attachments']];
Error message
[401] Client error: `POST https://api.xero.com/api.xro/2.0/Contacts/134ab308-7d07-4c2d-a770-c01325947ede` resulted in a `401 Unauthorized` response: {"Type":null,"Title":"Unauthorized","Status":401,"Detail":"AuthenticationUnsuccessful","Instance":"94b725d8-435b-4a98-be (truncated...)
It is working when I create or update invoices with almost identical request. Even identical bearer token. Also creating new contact if I pass that with new invoice. But not updating already existing contact. How can I fix this?
The answer is in the 401 response: "Title":"Unauthorized". You're not unauthenticated, you're unauthorised.
From the docs:
When your app is requesting authorisation from a user it will need to ask for a set of scopes. These scopes will be displayed to the user and describe what data your app will be able to access.
You supply the relevant scopes when requesting authentication with OAuth.
Your OAuth request (i.e. when you request authentication from the user) will be missing the accounts.contacts scope. Add it, and you should be able to successfully call those API endpoints.

Issue with Google Distance Matrix URL while making off-browser requests

https://maps.googleapis.com/maps/api/distancematrix/json?units=metric&origins=Neuh%C3%B6fer+Damm+110,Germany&destinations=Vogelweide+8,Hamburg&key=YOUR_API_KEY
The API Key is having the distance matrix API linked to it. If you use your own key, you will see the request in the browser working properly. However, the moment you make an HTTP::get(<>) in the code it returns you 404. Currently, I'm using PHP Laravel and follow the documentation.
Here is the request code:
$response = Http::get('https://maps.googleapis.com/maps/api/distancematrix/json
?units=metric
&origins=Neuh%C3%B6fer+Damm+110,Germany
&destinations=Vogelweide+8,Hamburg
&key=MyKey`enter code here`'
);
and here is the given response:
Illuminate\Http\Client\Response {#576 ▼
#response: GuzzleHttp\Psr7\Response {#618 ▼
-reasonPhrase: "Not Found"
-statusCode: 404
-headers: array:8 [▼
"Date" => array:1 [▶]
"Content-Type" => array:1 [▶]
"Server" => array:1 [▶]
"Content-Length" => array:1 [▶]
"X-XSS-Protection" => array:1 [▶]
"X-Frame-Options" => array:1 [▶]
"Server-Timing" => array:1 [▶]
"Alt-Svc" => array:1 [▶]
]
-headerNames: array:8 [▶]
-protocol: "1.1"
-stream: GuzzleHttp\Psr7\Stream {#617 ▶}
Are you experiencing the same issue? The main question then is: What do we miss so we get NotFound when we use the same valid link? Maybe headers? Maybe something else? I'll keep searching for an more proper answer, but meanwhile, I hope that helps
Do not use new-lines as the empty space might be added to the request.
Instead of tossing in the code one huge unreadable line of URL with tons of parameters, try using the GET Request Query Parameters. I think most HTTP clients can do that. In the case of using Laravel PHP Framework, the whole thing should look like that:
$response = Http::get('https://maps.googleapis.com/maps/api/distancematrix/json', [
'units' => 'metric',
'origins' => 'Neuhöfer Damm 110,Germany',
'destinations' => 'Vogelweide 8,Hamburg',
'key' => YOUR_API_KEY'
]);
Enjoy your coding :)

Google Indexing API calls keep returning 403 codes

I am trying to send vacancies to the Google Indexing API whenever we receive them from our vacancy provider. However, despite having set all the appropriate permissions, I keep receiving 403 status codes.
I have followed the "Prerequisites for the Indexing API" guide and created a Service account. In my case, I have 4 entries for the domain in Google Search Console, so I followed MarcQuay's answer and added the service account to all 4 entries.
I use the following code to implement the Google API Client and make my calls. The method sendGoogleRequest() is called for every vacancy that we receive.
// Google API setup
function sendGoogleRequest(array $aData, int $sStatus = 0)
{
global $DOMAIN, $GOOGLE_AUTH_CONFIG, $GOOGLE_AUTH_SCOPE, $GOOGLE_API_ENDPOINT;
$googleClient = new Google_Client();
$googleClient->useApplicationDefaultCredentials();
$googleClient->addScope([$GOOGLE_AUTH_SCOPE]);
// Update Google Indexing API. This will notify Google that the URL should be crawled again.
$httpClient = $googleClient->authorize();
$endpoint = $GOOGLE_API_ENDPOINT;
$sJobUrl = $DOMAIN . '/vacancies/' . $aData['url'];
$sType = "";
if (!empty($sStatus)) {
switch ($sStatus) {
case 1:
$sType = "URL_UPDATED";
break;
case 2:
$sType = "URL_DELETED";
break;
}
}
$content = "{
\"url\": \"$sJobUrl\",
\"type\": \"$sType\"
}";
$response = $httpClient->post($endpoint, ['body' => $content]);
$status_code = $response->getStatusCode();
return $status_code;
}
I have tried debugging it and it seems that '$credentials' is empty in $googleClient->authorize()
$authHandler = $this->getAuthHandler();
if ($credentials) {
$callback = $this->config['token_callback'];
$http = $authHandler->attachCredentials($http, $credentials, $callback);
} elseif ($token) {
$http = $authHandler->attachToken($http, $token, (array) $scopes);
} elseif ($key = $this->config['developer_key']) {
$http = $authHandler->attachKey($http, $key);
}
return $http;
However I have no idea what could be the cause of this.
Using this code returns a '403' for every call. After having browsed the internet for quite some time now, I can seem to find a definite answer, except for 'make sure the service account is an owner' but as previously stated, this is already the case.
I hope someone can help me out, since I have been stuck on this for longer than I'd like to admit.
If any more information is required I'll gladly update my answer.
EDIT: As per request, here is the full error message return by the $httpClient->post() call.
Response {#268 ▼
-reasonPhrase: "Forbidden"
-statusCode: 403
-headers: array:11 [▼
"Vary" => array:3 [▼
0 => "X-Origin"
1 => "Referer"
2 => "Origin,Accept-Encoding"
]
"Content-Type" => array:1 [▼
0 => "application/json; charset=UTF-8"
]
"Date" => array:1 [▼
0 => "Tue, 24 Sep 2019 11:25:29 GMT"
]
"Server" => array:1 [▼
0 => "ESF"
]
"Cache-Control" => array:1 [▼
0 => "private"
]
"X-XSS-Protection" => array:1 [▼
0 => "0"
]
"X-Frame-Options" => array:1 [▼
0 => "SAMEORIGIN"
]
"X-Content-Type-Options" => array:1 [▼
0 => "nosniff"
]
"Alt-Svc" => array:1 [▼
0 => "quic=":443"; ma=2592000; v="46,43,39""
]
"Accept-Ranges" => array:1 [▼
0 => "none"
]
"Transfer-Encoding" => array:1 [▼
0 => "chunked"
]
]
-headerNames: array:11 [▼
"vary" => "Vary"
"content-type" => "Content-Type"
"date" => "Date"
"server" => "Server"
"cache-control" => "Cache-Control"
"x-xss-protection" => "X-XSS-Protection"
"x-frame-options" => "X-Frame-Options"
"x-content-type-options" => "X-Content-Type-Options"
"alt-svc" => "Alt-Svc"
"accept-ranges" => "Accept-Ranges"
"transfer-encoding" => "Transfer-Encoding"
]
-protocol: "1.1"
-stream: Stream {#256 ▼
-stream: stream resource #123 ▼
wrapper_type: "PHP"
stream_type: "TEMP"
mode: "w+b"
unread_bytes: 0
seekable: true
uri: "php://temp"
options: []
}
-size: null
-seekable: true
-readable: true
-writable: true
-uri: "php://temp"
-customMetadata: []
}
}
I ended up fixing this issue by using a Service account as AuthConfig
// Initialize Google_Client to submit vacancies to Indexing API
$googleClient = new Google_Client();
$googleClient->setAuthConfig($GOOGLE_AUTH_CONFIG);
$googleClient->addScope([$GOOGLE_AUTH_SCOPE]);
$googleClient->setAccessType("offline");
$googleClient is used for every call
function sendGoogleRequest(array $aData, int $sStatus = 0, Google_Client $google_Client) {
global $DOMAIN;
$sJobUrl = 'https://MY-DOMAIN.com/' . $aData['url'];
$sType = "";
if (!empty($sStatus)) {
switch ($sStatus) {
case 1:
$sType = "URL_UPDATED";
break;
case 2:
$sType = "URL_DELETED";
break;
}
}
$urlNotification = new Google_Service_Indexing_UrlNotification();
$urlNotification->setType($sType);
$urlNotification->setUrl($sJobUrl);
$indexingService = new Google_Service_Indexing($google_Client);
$response = $indexingService->urlNotifications->publish($urlNotification);
return $response;
}
This script is then called once per day and posts every URL to the Indexing API. To not exceed the limit of 250 requests per day, make sure you exclude vacancies that should no longer be indexed.

Guzzle and Form + Basic Auth issues with Laravel

I have been looking into grabbing some data from Livecoding.tv, and i am currently reusing my Oauth2 controller that i used for the twitch API, which should be pretty straightforward.
If someone does not know, the flow used by Oauth is the following:
Redirect user to third party Oauth link with your app code.
User Authorizes.
User redirected to your website again, with an Authorized token, that you can then post to the third party to get your refresh token etc.
Now on step 3, i am running into some problems. Here is the description and example by the developer:
Getting token: https://www.livecoding.tv/o/token/
Header
A HTTP Basic auth, using the application_code as username, and application_secret as password, as seen in the example below.
POST Body
code=EXAMPLE Token gotten from redirect
grant_type=Your grant type (authorization_type)
redirect_uri=Your redirect URL
And here is an example cURL request from the documentation of a working curl request.
curl -X POST -d "grant_type=authorization_code&code=Php4iJpXGpDT8lCqgBcbfQ2yzhB0Av&client_id=vCk6rNsC&redirect_uri=http://localhost/externalapp" -u"vCk6rNsC:sfMxcHUuNnZ" https://www.livecoding.tv/o/token/
So i tried to make this in Postman(https://www.getpostman.com/), which worked out of the box, i then asked Postman to convert this to PHP, to see if i had missed something. Here is the outcome of the working Postman request:
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://www.livecoding.tv/o/token/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "code=SOMECODE&client_id=SOMECLIENTID&redirect_uri=SOMEURL&grant_type=authorization_code",
CURLOPT_HTTPHEADER => array(
"authorization: Basic U09NRVVTRVI6U09NRVBBU1NXT1JE",
"cache-control: no-cache",
"content-type: application/x-www-form-urlencoded",
"postman-token: c8df4bbc-cbd0-73eb-df35-80210989db33"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
I was pretty happy this worked out right away, so i just needed to make a Guzzle client do the same, which already works for the Twitch Oauth, here is my code for that:
{
$providers = ServiceProvider::findOrFail(2);
$client = new Client([
'base_uri' => $providers->oauth_url . '/token/',
'form_params' => [
'code' => $token,
'grant_type' => 'authorization_code',
'client_id' => $providers->application_code,
'redirect_uri' => $providers->redirection_url
],
'auth' => [
'somestring',
'someotherstring',
],
'headers' => [
'Content type' => 'application/x-www-form-urlencoded',
'cache-control' => 'no-cache'
]
]);
$response = $client->request('POST');
return ($response);
}
This would just return me with a 401, so i decided to do some debugging, and here is the request that comes, if i stop it right before it is sent:
Client {#637 ▼
-config: array:10 [▼
"base_uri" => Uri {#676 ▼
-scheme: "https"
-userInfo: ""
-host: "www.livecoding.tv"
-port: null
-path: "/o/token/"
-query: ""
-fragment: ""
}
"form_params" => array:4 [▼
"code" => "SOMECODE"
"grant_type" => "authorization_code"
"client_id" => "SOMECLIENTID"
"redirect_uri" => "http://REDIRECTURI"
]
"auth" => array:2 [▼
0 => "SOMECLIENTID"
1 => "SOMECLIENTSECRET"
]
"headers" => array:3 [▼
"Content type" => "application/x-www-form-urlencoded"
"cache-control" => "no-cache"
"User-Agent" => "GuzzleHttp/6.2.1 curl/7.26.0 PHP/5.6.27-1~dotdeb+7.1"
]
"handler" => HandlerStack {#664 ▼
-handler: Closure {#671 ▼
class: "GuzzleHttp\Handler\Proxy"
parameters: {▼
$request: {▼
typeHint: "Psr\Http\Message\RequestInterface"
}
$options: {▼
typeHint: "array"
}
}
use: {▼
$default: Closure {#669 ▼
class: "GuzzleHttp\Handler\Proxy"
parameters: {▼
$request: {▼
typeHint: "Psr\Http\Message\RequestInterface"
}
$options: {▼
typeHint: "array"
}
}
use: {▼
$default: CurlMultiHandler {#634 ▼
-factory: CurlFactory {#667 ▼
-handles: []
-maxHandles: 50
}
-selectTimeout: 1
-active: null
-handles: []
-delays: []
}
$sync: CurlHandler {#666 ▼
-factory: CurlFactory {#665 ▼
-handles: []
-maxHandles: 3
}
}
}
file: "/LARAVELPATH/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php"
line: "25 to 29"
}
$streaming: StreamHandler {#670 ▼
-lastHeaders: []
}
}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php"
line: "49 to 53"
}
-stack: array:4 [▼
0 => array:2 [▼
0 => Closure {#672 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "54 to 69"
}
1 => "http_errors"
]
1 => array:2 [▼
0 => Closure {#673 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "148 to 150"
}
1 => "allow_redirects"
]
2 => array:2 [▼
0 => Closure {#674 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "27 to 43"
}
1 => "cookies"
]
3 => array:2 [▼
0 => Closure {#675 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "216 to 218"
}
1 => "prepare_body"
]
]
-cached: null
}
"allow_redirects" => array:5 [▼
"max" => 5
"protocols" => array:2 [▼
0 => "http"
1 => "https"
]
"strict" => false
"referer" => false
"track_redirects" => false
]
"http_errors" => true
"decode_content" => true
"verify" => true
"cookies" => false
]
}
As you can see here, there is just a parameter called 'auth' in here, and not the basic auth that is documented in Guzzle here: http://docs.guzzlephp.org/en/latest/request-options.html#auth
Though i would mention that the correct values are entered.
For the debugging i just did dd($client), i do not know if this will give me all the answers?
So the other possibility is to base64 encode it (like it is done in basic auth in general), and add a "Authorization" header manually, i tried that, but i am unaware if i am doing it correctly when i do:
$credentials = base64_encode($clientvariable . ':' . $clientsecretvariable)
Would that be the correct way? Though i would rather use this as a last resort if i do not get the auth parameter to work in Guzzle.
I am aware of the other Stackoverflow questions about Guzzle and basic auth, and yes i have read them, hopefully i have provided with enough information to show that.
The problem here was related to how i return the response from Guzzle.
Since i just returned $response, which is just the stream, the browser never got anything returned, and since chrome auto refresh, he runs the request twice, and on the second time, i would have just my code twice, which would have returned an error.
I ended up going crazy about this, until i changed browser, and saw that nothing ever happened, which pointed me in the right direction.

Categories