Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to send email via Gmail SMTP in secure way

The following code https://stackoverflow.com/a/3649148 works all the while, until it broken recently when Google has changed their security policy.

I received mail from Google

Hi xxx, Someone just tried to sign in to your Google Account [email protected] from an app that doesn't meet modern security standards.

We strongly recommend that you use a secure app, like Gmail, to access your account. All apps made by Google meet these security standards. Using a less secure app, on the other hand, could leave your account vulnerable. Learn more.

Google stopped this sign-in attempt, but you should review your recently used devices:

I look at https://support.google.com/accounts/answer/6010255?hl=en-GB

I was wondering, what is the way to implement a correct sign-in attempt, in order to continue to send email via Gmail SMTP, with 0 configuration from user side?

like image 819
Cheok Yan Cheng Avatar asked Aug 06 '15 04:08

Cheok Yan Cheng


1 Answers

OAuth 2.0

I had to implement this on a PHP-based webpage because our server does not have a mail server and we are using google's one instead. It is likely Google will cut any unauthorized access to their services in the future; and we want to have a future-proof solution. I believe porting this solution to other languages (such as tagged Java) should be no big issue.

Requirements:

Google mail account with Cloud Console enabled, a web domain with https enabled, PHP 5.4 or greater with JSON extension (bundled since v5.2, but sometimes not installed anyway - we presume it is already installed) and a lot of patience.

Additionally, we need PHP Google API Client library, that can be obtained using:

  • server's command-line interface (CLI) and Composer dependency manager. The advantages are that you will receive updates automatically. The Google API library is huge and there are ways to exclude unwanted parts.

OR

  • copy the library manually from its repository. The advantages are that you need only access to your webhosting cloud service, useful in case you cannot use the Composer manager.

1) Project in Google Console

First, you need to create a project in google console. Perform this setup on an account you want to associate with your webpage mail-sending application - YES, this is going to be an application - from Google's point of view. Once logged in: in the header menu in project selection, select and create a new project (mine is called Gmail API from now on). I will be also using a (non-existing) webpage called gmapi.xy.

console

Once a new project is created, go to the Library section, find Gmail API and enable it.

2) Credentials

You obviously need some authentication data. The enablement of API should redirect you to Gmail API console interface - select Credentials in the left menu:

Create new OAuth client ID

  • but you've been told to first specify your product title on OAuth consent screen -selecting create new OAuth client ID will show you the consent screen tab in the left menu.

2 a) Create product (OAuth consent screen)

You need to fill in name, support email, scope, authorized domains and OAuth limits

Scope : You need to specify all privileges granted to anyone who will authenticate to the Gmail API:

  • click 'Add Scope'
  • scroll down and enable all desired scopes in Gmail API section (in my case, I wanted to send mails only, so I enabled also https://www.googleapis.com/auth/gmail.send scope.

Authorized domains : Fill in the domain you want to send the emails from:

  • add authorized domains - these will be allowed to access the api (no protocol, only top-level: gmapi.xy)
  • add app homepage, privacy policy and terms of service links - same as above in my case (but a full address: https://gmapi.xy OR https://gmapi.xy/policy etc.)

OAuth grant limits : I am satisfied with default setup so no changes (for more information on this topic see https://developers.google.com/analytics/devguides/config/mgmt/v3/limits-quotas)

Save changes : Save the form. We've asked for additional scopes - likely we will see a warning:

This app isn't verified.

The OAuth consent screen that is presented to the user may show the warning "This app isn't verified" if it is requesting scopes that provide access to sensitive user data. These applications must eventually go through the verification process to remove that warning and other limitations. During the development phase you can continue past this warning by clicking Advanced > Go to {Project Name} (unsafe).

Once your app is running, alive, public and not in the development phase, submit the application for verification (button next to Save). The verification of your new app might take up to several weeks.

2 b) Create the credentials - again

Now we are able to create the credentials - as before, select Credentials in the left menu. Create new OAuth client ID.

  • select Web application as a type
  • populate authorized javascript origins with your URIs, in my case https://gmapi.xy and https://www.gmapi.xy
  • populate authorized redirect URIs - google will ask the user for privilages, and you have to specify the URIs the google API is allowed to redirect back to. In my case, I am using test.php (having https://www.gmapi.xy/test.php URI) that is being redirected to itself. Thus, i will add there https://www.gmapi.xy/test.php and https://gmapi.xy/test.php.

There are many problems with allowed redirect URLs, you must not end with / sign if your real URI does not have one, you must specify PORT number if you use other than default one... see this thread for more information. There is a checklist for you:

  • http or https?
  • & or &?
  • trailing slash(/) or open ?
  • (CMD/CTRL)+F, search for the exact match in the credential page. If not found then search for the missing one.
  • Wait until google refresh it. May happen in each half an hour if you are changing frequently or it may stay in pool. For my case it was almost half an hour to take effect.
  • Did you re-import the credentials.json file upon changing the values in your OAuth ID?

NOTE: having multiple allowed URIs can harm your script - after successful authorization, you can still get this error after being redirected back to your webpage.

  • save the form (you don't have to copy the credentials you've been provided with upon successful OAuth ID creation - close the popup)
  • download JSON credentials file - in OAuth 2.0 Client IDs section, click the arrow icon

3 a) Setup PHP - with Composer

  • install google API library via Composer: composer require google/apiclient:^2.0
  • optionally: run the Google_Task_Composer::cleanup task and specify the services you want to keep in composer.json:
    {
        "require": {
            "google/apiclient": "^2.7"
        },
        "scripts": {
            "post-update-cmd": "Google_Task_Composer::cleanup"
        },
        "extra": {
            "google/apiclient-services": [
                "Drive",
                "YouTube"
            ]
        }
    }

NOTE: there seems to be issues with this approach that has not been resolved yet. Therefore I did not try this feature. I also could not find available services name list, so it would be good if someone found it and added a link.

3 b) Setup PHP - without Composer

Download any .zip - a stable version from releases of your desired PHP version and place it somewhere in the website (upload the .zip and extract it there).

4 Include the library, your credentials and prepare for authentication

Upload also the JSON credentials file.

SECURITY NOTICE: place it somewhere safe: either remove the R/W privileges for anyone except owner or protect this file using .htaccess!

Then simply include autoload.php (locate the library on the server for its path, it should be in vendor folder) and provide the credentials for authentication.

require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setAuthConfig('/path/to/client_credentials.json');

Furthermore, you have to ask for the scope privileges and obtain a token from the service. The available scope list is available here. Also, see this thread for more examples on scopes.

$client->setPrompt("consent");
$client->setScopes(array(
    'https://www.googleapis.com/auth/gmail.send'
     //add more if you want to have them, or add
     // "https://mail.google.com/" to read, compose, send, delete mails
));
$client->setAccessType('offline');
$client->setIncludeGrantedScopes(true);
$tokenPath = 'where/you/want/to/store/token.json';


// Get new token - see redirect below
if (isset($_GET['code'])) {
    $accessToken = $client->fetchAccessTokenWithAuthCode($_GET['code']);
    // Save the token to a file.
    if (!file_exists(dirname($tokenPath))) {
        mkdir(dirname($tokenPath), 0700, true);
    }
    file_put_contents($tokenPath, json_encode($accessToken));
    $client->setAccessToken($accessToken);
} else if (file_exists($tokenPath)) {
    // Get the saved token
    $accessToken = json_decode(file_get_contents($tokenPath), true);
    $client->setAccessToken($accessToken);
}

// If there is no previous token or it's expired.
if ($client->isAccessTokenExpired()) {
    // Refresh the token if possible, else fetch a new one.
    $refreshToken = $client->getRefreshToken();
    if ($refreshToken) {
        $client->fetchAccessTokenWithRefreshToken($refreshToken);
    } else {
        // Get the token - redirect to the same page
        $redirect_uri = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
        $client->setRedirectUri($redirect_uri);
        $auth_url = $client->createAuthUrl();

        // Actually GO to the authentication (and authorization) url
        header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
    }
}

At first, the webpage redirects us to google where we need to grant access for our application to the desired email to send messages with. Once approved, a token file is created that we store in a file. When the token expires, a new token should be created automatically with fetchAccessTokenWithRefreshToken() - more here.

But just in case, I would advise something like:

// Get the token - redirect to the same page
if ( user_not_administrator ) {
    //TODO: redirect user to some error page and get yourself a notification
    exit;
}

$redirect_uri = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];

5 Send emails!

function createMessage($sender, $to, $subject, $messageText) {
    $message = new Google_Service_Gmail_Message();

    $rawMessageString = "From: <{$sender}>\r\n";
    $rawMessageString .= "To: <{$to}>\r\n";
    $rawMessageString .= 'Subject: =?utf-8?B?' . base64_encode($subject) . "?=\r\n";
    $rawMessageString .= "MIME-Version: 1.0\r\n";
    $rawMessageString .= "Content-Type: text/html; charset=utf-8\r\n";
    $rawMessageString .= 'Content-Transfer-Encoding: quoted-printable' . "\r\n\r\n";
    $rawMessageString .= "{$messageText}\r\n";

    $rawMessage = strtr(base64_encode($rawMessageString), array('+' => '-', '/' => '_'));
    $message->setRaw($rawMessage);
    return $message;
}

function sendMessage($service, $userId, $message) {
    try {
        return $service->users_messages->send($userId, $message);
    } catch (Exception $e) {
        //todo error - use $e->getMessage();
    }
    return null;
}

sendMessage(new Google_Service_Gmail($client), 'me', createMessage(...));

Sources

  • https://github.com/googleapis/google-api-php-client
  • https://developers.google.com/gmail/api/quickstart/php
  • https://github.com/googleapis/google-api-php-client/blob/master/docs/oauth-server.md#creating-a-service-account
  • https://github.com/googleapis/google-api-php-client/blob/master/docs/oauth-web.md
  • https://blog.mailtrap.io/send-emails-with-gmail-api/
like image 54
Jiří Avatar answered Oct 15 '22 12:10

Jiří