BlackBerry Z10, Gmail and 2-Step Authentication

Update: If you’ve arrived here looking for solutions to Gmail problems generally on the new BlackBerry OS 10, you might want to read my more recent article, Gmail + Google Apps on the BlackBerry Z10.

BlackBerry Bold 9900 and Z10
The old (Bold 9900) and the new (Z10) – any family resemblance is purely coincidental
One of the perks of my job is getting to try out some new mobile phones, once they’ve been released by my company’s preferred carrier. This week I took delivery of a BlackBerry Z10. It’s a really nice device, but given that my normal phone is a Samsung Galaxy Note 2, I’m afraid I became a little bored of the Z10 within 10 minutes of powering it up. (Yeah yeah, I know, first world problems.)

Inevitably with BlackBerry OS 10 (which is a stonkingly good OS, by the way), one of the problems was going to be that the apps are lagging behind mainstream Apple and Android offerings. Google’s Play store and Apple’s App Store have apps outnumbering BlackBerry apps by a factor of 10. Whether BlackBerry will catch up is a question not even the most far-seeing analysts can predict with confidence.

In the meantime, there will be some speed bumps encountered by dedicated BlackBerry aficionados, such as the one I ran into when attempting to set up my Gmail account today. Like all security conscious users, I have 2-step authentication enabled on my Gmail account. That means that whenever I log into Gmail using a new device or application, a text message is sent to my phone containing a one-time authorisation code, which then needs to be entered into whichever application is trying to use my Gmail credentials.

The email app on BlackBerry 10 is not (yet) capable of handling this process. That means you need to use one of the alternative methods of authentication. To start, you must (preferably from a computer) log into your Google 2-step verification page. Once there, you can be forgiven for thinking that Google has got you covered. There is, after all, a link saying “BlackBerry”:

2-step verification

If you follow the link, you’re instructed to browse to m.google.com/authenticator and download the Authenticator app to your phone, following which everything will be ticketyboo. Except it won’t. You are taken to a largely blank web page. Buried deep within Google’s online help is the reason: “To use Google Authenticator on your BlackBerry device, you must have OS 4.5-7.0.” No OS 10 support then. Bother.

Never fear. Go back to the 2-step verification page and instead choose the Manage application-specific passwords link. Go to the bottom of that page (“Step 1 of 2: Generate a new application-specific password”). Enter a name (like “BlackBerry Z10”) and click “Generate”. You’ll be given a code to enter in the IMAP password and SMTP password boxes within the email setup on your phone. Once you’ve done that, you’re good to go.

Rackspace API for CodeIgniter

Logo_lockup_version-2 SPOTRackspace is a great email hosting company, providing, amongst other things, a handy API for creating bespoke email solutions. The exercise of integrating that API into your application is of course left to the end user. I’ve spent some time working on a Rackspace API library for the PHP programming framework, CodeIgniter. This is no functionally complete – I have only implemented the interfaces that I needed – but it should provide a useful springboard for your own projects.

Configuration

In /system/application/config/RackspaceAPI.php:

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

$config&#91;'user_key'&#93;   = 'your user key';
$config&#91;'secret_key'&#93;     = 'your secret key';
$config&#91;'user_agent'&#93;     = 'name of your app';
$config&#91;'api_version'&#93;    = 'v0'; // amend if necessary
$config&#91;'rackspace_host'&#93; = 'api.emailsrvr.com'; // amend if necessary

/* End of file RackspaceAPI.php */
/* Location: ./system/application/config/RackspaceAPI.php */&#91;/php&#93;

<h1>Library</h1>
<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
 * Uses curl and pecl_http
 */
class Rackspace_API {
    
  /**
   * Store recent http_message
   * @var object
   */
  protected $_http_message;
  
  /**
    * CI object
    * @var object
    */
  protected $_ci;

  /**
   * Rackspace config items
   */
  protected $_user_key;
  protected $_secret_key;
  protected $_user_agent;
  protected $_api_version;
  protected $_rackspace_host;
  
  function __construct() {
    $this->_ci =& get_instance();
    $this->_ci->config->load('RackspaceAPI', TRUE);
    $this->_user_key = $this->_ci->config->item('user_key', 'RackspaceAPI');
    $this->_secret_key = $this->_ci->config->item('secret_key', 'RackspaceAPI');
    $this->_user_agent = $this->_ci->config->item('user_agent', 'RackspaceAPI');
    $this->_api_version = $this->_ci->config->item('api_version', 'RackspaceAPI');
    $this->_rackspace_host = $this->_ci->config->item('rackspace_host', 'RackspaceAPI');
  }


  /**
   * Get info about a domain
   * @param string $domain
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function getDomainInfo($domain) {
    return $this->genericGet('/customers/me/domains/'.$domain);
  }

  
  /**
   * Get all domain names
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | array (domains)
   */
  public function getDomains() {
    $obj = $this->genericGet('/customers/me/domains');
    if(!$obj->error){
      // Reformat into an array of domains
      foreach($obj->result->domains as $domain) {
        $domains[]=$domain->name;
      }
      $obj->result = $domains;
    }
    return $obj;
  }


  /**
   * Get info about a mailbox ([email protected]$id)
   * @param string $domain
   * @param string $id
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function getMailboxInfo($domain, $id) {
    return $this->genericGet('/customers/me/domains/'.$domain.'/rs/mailboxes/'.$id);
  }
   
 
  /**
   * Used by Get functions above - generalised use case
   * @param string $url - see the API; constructed by the calling function
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  private function genericGet($url) {
    $this->get(
        $url,
        'application/json');
    if($this->_http_message->getResponseCode() == 200) {
      // Call worked.  JSON is missing enclosing brackets, apparently needed by json_decode
      $json = '['.$this->_http_message->getBody().']';
      if(is_string($json)) {
        $obj = json_decode($json);
        $result->error = false;
        $result->result = $obj[0];
      } else {
        // JSON failure
        $result->error = true;
        $result->result = 'Failed to parse JSON';
      }
    } else {
      // API call failed
      $result->error = true;
      $result->result = $this->_http_message->getHeader("x-error-message");
    }
    return $result;
  }
  

  /**
   * Create a mailbox ([email protected]$id)
   * @param string $domain
   * @param string $id
   * @param string $first: First name
   * @param string $last: Last name
   * @param string $name: Display as
   * @param string $office: Name of office/profit centre
   * @param string $locno: Office/profit centre number
   * @param string $password
   * @param string $fwd: comma-separated forwarding address(es) - max 4 off domain
   * @param string $save: save forwarded email - 'true' or 'false'
   * saveForwardedEmail
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function addMailbox($domain, $id, $first, $last, $name, $office,
          $locno, $password, $fwd, $save='true') {
    $fields = array(
        'password' => $password, 
        'emailForwardingAddresses' => $fwd,
        'firstName' => $first,
        'lastName' => $last,
        'displayName' => $name,
        'saveForwardedEmail' => $save,
        'organization' => $office,
        'organizationUnit' => $locno);
    return $this->genericPost( '/customers/me/domains/'.$domain.'/rs/mailboxes/'.$id, $fields);
  }


  /**
   * Used by Post functions above - generalised use case
   * Note: Rackspace API suggests use POST to add, PUT to edit
   * @param string $url - see the API; constructed by the calling function
   * @param array $fields - data to be POSTed
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  private function genericPost($url, $fields) {
    $this->post(
        $url,
        $fields,
        'application/json');
    if($this->_http_message->getResponseCode() == 200) {
      $result->error = false;
      $result->result = $this->_http_message->getBody();
    } else {
      // API call failed
      $result->error = true;
      $result->result = $this->_http_message->getHeader("x-error-message");
    }
    return $result;
  }


  /**
   * Edit user's forwarding
   * @param string $domain
   * @param string $id
   * @param string $fwd: comma-separated forwarding address(es) - max 4 off domain
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function changeForwarding($domain, $id, $fwd) {
    $fields = array(
        'emailForwardingAddresses' => $fwd
        );
    return $this->genericPut( '/customers/me/domains/'.$domain.'/rs/mailboxes/'.$id, $fields);
  }
  
    
  /**
   * Edit user's location
   * @param string $domain
   * @param string $id
   * @param string $office: Name of office/profit centre
   * @param string $locno: Office/profit centre number
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function changeLocation($domain, $id, $office, $locno) {
    $fields = array(
        'organization' => $office,
        'organizationUnit' => $locno);
    return $this->genericPut( '/customers/me/domains/'.$domain.'/rs/mailboxes/'.$id, $fields);
  }
  
    
  /**
   * Edit user's name
   * @param string $domain
   * @param string $id
   * @param string $first: First name
   * @param string $last: Last name
   * @param string $name: Display as
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function changeName($domain, $id, $first, $last, $name) {
    $fields = array(
        'firstName' => $first,
        'lastName' => $last,
        'displayName' => $name);
    return $this->genericPut( '/customers/me/domains/'.$domain.'/rs/mailboxes/'.$id, $fields);
  }
  
    
  /**
   * Edit user's password
   * @param string $domain
   * @param string $id
   * @param string $password
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function changePassword($domain, $id, $password) {
    $fields = array(
        'password' => $password);
    return $this->genericPut( '/customers/me/domains/'.$domain.'/rs/mailboxes/'.$id, $fields);
  }
  
    
  /**
   * Used by Put functions above - generalised use case
   * Note: Rackspace API suggests use PUT to edit, POST to add
   * @param string $url - see the API; constructed by the calling function
   * @param array $fields - data to be PUT
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  private function genericPut($url, $fields) {
    $this->put(
        $url,
        $fields);
    if($this->_http_message->getResponseCode() == 200) {
      $result->error = false;
      $result->result = $this->_http_message->getBody();
    } else {
      // API call failed
      $result->error = true;
      $result->result = $this->_http_message->getHeader("x-error-message");
    }
    return $result;
  }


  /**
   * Delete a mailbox
   * @param string $domain
   * @param string $id
   * @return stdClass Object ( 'error'  => bool,
   *                           'result' => string (error message) | stdClass Object
   */
  public function deleteMailbox($domain, $id) {
    return $this->genericDelete("/customers/me/domains/$domain/rs/mailboxes/$id");
  }
  
  
  /**
   * Used by Get functions above - generalised use case
   * @param string $url - see the API; constructed by the calling function
   * @return stdClass Object ( 'error'  => bool,
   *                           ['result' => string (error message)]
   */
  private function genericDelete($url) {
    $this->delete($url);
    if($this->_http_message->getResponseCode() == 200) {
      // Call worked.
      $result->error = false;
    } else {
      if($this->_http_message->getResponseCode() == 500) {
        // Internal server error
        $result->error = true;
        $result->result = 'An internal server error occurred deleting  object.  Url: '.$url;
      } else {
        // API call failed
        $result->error = true;
        $result->result = $this->_http_message->getHeader("x-error-message");
        
      }
    }
    return $result;
  }
  

  
  // The remainder of this file is mostly lifted from Rackspace's examples: http://api-wiki.apps.rackspace.com/api-wiki/index.php/PHP_Examples_(Rest_API)
  private function get($url_string, $format) {
      $headers = array("Accept: $format");
      $curl_session = self::construct_session($url_string, $headers);
      $this->_http_message = self::send_request($curl_session);
  }

  private function post($url_string, $fields, $format) {
      $headers = array("Accept: $format");
      $curl_session = self::construct_session($url_string, $headers);
      curl_setopt($curl_session, CURLOPT_POST, true);
      curl_setopt($curl_session, CURLOPT_POSTFIELDS, $fields);
      $this->_http_message = self::send_request($curl_session);
  }

  private function put($url_string, $fields) {
      $curl_session = self::construct_session($url_string, array());
      curl_setopt($curl_session, CURLOPT_CUSTOMREQUEST, 'PUT');
      curl_setopt($curl_session, CURLOPT_POSTFIELDS, $fields);
      $this->_http_message = self::send_request($curl_session);
  }
  
  private function delete($url_string) {
      $curl_session = self::construct_session($url_string, array());
      curl_setopt($curl_session, CURLOPT_CUSTOMREQUEST, 'DELETE');
      $this->_http_message = self::send_request($curl_session);
  }

  private function send_request($curl_session) {
      $response = curl_exec($curl_session);
      curl_close($curl_session);
      /* Reponse string may contain two HTTP sessions, if there was an initial
         "HTTP/1.1 100 Continue" response.  So strip that first response out.  Eg:
                  HTTP/1.1 100 Continue
                  Via: 1.1 [proxy]

                  HTTP/1.1 400 Bad Request
                  Via: 1.1 [proxy]
                  Connection: Keep-Alive
                  Proxy-Connection: Keep-Alive      
                  ...     
       * 
       */
      $response = preg_replace('|HTTP/1.1 100.*HTTP/1.1|isU', 'HTTP/1.1', $response);
      return new HttpMessage($response);
  }

  private function construct_session($url_string, $existing_headers) {
      $headers = array_merge(
              self::authorization_headers(), $existing_headers);
      $url = self::construct_uri($url_string);
      $curl_session = curl_init($url);
      curl_setopt($curl_session, CURLOPT_HEADER, true);
      curl_setopt($curl_session, CURLOPT_HTTPHEADER, $headers);
      curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, true);
      return $curl_session;
  }

  private function authorization_headers() {
      $time_stamp = date('YmdHis');
      $data_to_sign = $this->_user_key . $this->_user_agent .
          $time_stamp. $this->_secret_key;
      $signature = base64_encode(sha1($data_to_sign, true));
      $headers = array();
      $headers[] = "User-Agent: " . $this->_user_agent;
      $headers[] = 'X-Api-Signature: ' .
          $this->_user_key . ":$time_stamp:$signature";
      return $headers;
  }

  private function construct_uri($url_string) {
      $url = 'http://' .  $this->_rackspace_host . '/' . $this->_api_version . $url_string;
      return $url;
  }
}

?>

Example

Example usage:

function testRackspace() {
    $this->load->library('Rackspace_API');
    $client = new Rackspace_API();
    $obj = $client->getMailboxInfo('somedomain.com', 'test.user');
    if($obj->error) {
      echo 'Error: '.$obj->result;
    } else {
      var_dump($obj);
    }
  }

Image copyright © Rackspace Ltd. All rights acknowledged.