2007-07-31

Mobile Gmail API

Did you know that Gmail provides a handy, light-weight API for gmail? Google's gmail midlet uses this API.

Mobile Gmail Basics

First thing to note, all responses from the server are in the form of UTF8 string lists. There is a 16 bit word (in network byte order) that represents the length of the string, followed by that many bytes. Then another 16 bit word for the next string, and so on. The response is usually terminated with an empty string (16 bit length is 0).

Secondly, all communication is done via POST requests to a single URL. This URL is https://mail.google.com/mail/m/12345 where 12345 is a random number, presumably to defeat proxy caches common with mobile phones. Please note that all communication is done over SSL.

Third, all requests from the server must contain the API version. Include "p=1.1" as the very first POST variable.


Logging In

Logging in is rather easy. You send a request to the gmail API with the following POST variables:

zym=l (that's a lowercase L)
user=$user
password=$password

That's pretty self explanatory.

The very first string in the server's response will be the Status. This will tell you if you were successful in logging in or not. The server will also set a bunch of cookies. You must save these cookies and send them back from this point on. This is what keeps you "logged in".

If Status is "E", then there was some sort of error.
The strings that follow the Status string are listed below.

error_type
error_string
CAPTCHA_token (optional)
CAPTCHA_image (optional)

error_type is the type of error, it will be either "C" for CAPTCHA required, or "B" for standard errors. If the error is "C" then CAPTCHA_token and CAPTCHA_image will be present. CAPTCHA_image contains the raw data for the image.

If you get a CAPTCHA required error, have the user fill out the captcha information, and send back the following POST variables:

zym=l (again, lowercase L)
captchaToken=$token
captchaAnswer=$answer

$token is the CAPTCHA_token from the server, $answer is the user's answer.

If the Status is "T" then you were successful in logging in, and the packet that is returned is the Inbox packet.

Inbox Packet

The Inbox packet is marked with a Status of "T". The strings that follow the Status string are listed below.

id_code
start_thread
num_threads
total_threads
num_unread
threads[]
num_labels
labels[]

start_thread is the index for the first message returned. num_threads is the number of threads returned. total_threads is the total number of threads in the inbox. In gmail, at the top you see "1-50 of 513". num_threads would be 50, start_thread would be 0, and total_threads is 513. num_unread is the number of unread messages in your inbox.

threads is an array of strings representing each thread. The strings in each thread are as follows:

is_read
is_starred
has_attachments
from
subject
time
url

is_read is "T" if the message has been read, "F" otherwise. is_starred is "T" if the thread has been starred, "F" otherwise. has_attachments is "T" if the thread has attachments, "F" otherwise. from is a user-friendly version of the participants of the thread, e.g. "Jon, me (5)". subject is the subject line of the thread. time is a user-friendly version of the time, e.g. "11:08 am" or "Jul 27". Finally, url contains the ID number for the thread. It is given as "?th=xxxxxxxxxxx" you'll want to strip out the "?th=" part.

Back to the inbox format for a second. num_labels is the number of labels you have set up. They are followed by an array of label strings. The label strings are pretty simple. They look like so:

label
num_unread

label is the label itself. num_unread is the number of unread messages in that label.


Contact List

Now let's look at how to retrieve the contact list. You send a simple request with the following POST variables: v=cl, pnl=$type $type is the type of contact list to retrieve. "f" is frequent. This is an abridge contact list with just the people you frequently mail. "a" is all, this is the entire contact list. The resulting packet is formatted the same, regardless of which type you request.

num_contacts
contacts[]

num_contacts is obviously the number of contacts in the following contact list. Each contact looks like so:

contact_name
contact_email

Pretty simple. More detailed contact information doesn't seem to be available.


Read a Thread


Reading a thread is fairly straightforward. Send a request with the following POST variables:

th=$thread_id
v=c

$thread_id is the id of the thread you wish to pull. Just like the Inbox, the first string is a Status. If it is "C" then you were successful in pulling a thread, if it is "E" then there was an error. The strings following the status string are below.

thread_id
thread_subject
num_messages
message_headers[]
body
num_attachments (optional)
attachments[] (optional)

thread_subject is the subject line for the thread. num_messages is the number of messages contained in this thread. This is followed by one or more message_headers. Then the body of the selected message is present. The body is represented by several strings, each starting with a colon ":". Each string represents a full line in the message, minus the line break. If there are attachments in the selected message, then num_attachments is the first string without a leading colon. This is followed by a number of attachment strings.

The message_header looks like so.

is_read
is_starred
has_attachments
from_friendly
to_friendly
date_friendly
from_address
b1
to_address
b2
b3
timestamp
subject
url
b4
from_address2
to_address2

is_read is "T" if the message has been read, "F" otherwise. is_starred is "T" if the message has been starred, "F" otherwise. has_attachments is "T" if the message has attachments, "F" otherwise. from_friendly is a user-friendly version of the from address. to_friendly is a user-friendly version of the to addresses. date_friendly is a user-friendly version of the message date. from_address is the raw from address. b1 is "b", I'm not sure what this represents. to_address is the raw to address. b2 and b3 are "b", I'm not sure what these represent. I believe b2 might contain cc: addresses, if present. timestamp is the actual timestamp for the message. subject is the subject for the message. url contains the id for the message, in the format "?d=u&n=nnn#m_xxxxxxx". "xxxxxx" is the message id. "nnn" is the message number in the thread. b4 is "b", and I'm not sure what it represents. from_address2 and to_address2 seem to be identical to from_address and to_address.

Each attachment has the following format:

attachment_filename
attachment_id
attachment_mimetype

Each of these is fairly self-explanatory.

Changing the selected message in a thread is fairly simple. You use the first part of the message url (the "d=u&n=nnn") part and pass those variables along with the regular read thread variables. It will return the same packet as above, only with the selected message body.


Previewing Attachments

As far as I can tell, the gmail mobile API doesn't allow you to download attachments, only to retrieve "previews" of the attachments. To do so, you issue a request with the following POST variables:

view=att
disp=vah
attid=$attachment_id
th=$message_id
j=$graphics_mode
w=$width
h=$height

$attachment_id is obviously the attachment id number you wish to view. $message_id is the "xxxxxxx" part of "#m_xxxxxx" in the message url. $graphics_mode is either "1" or "0". 1 means you can handle image attachments. 0 means text only. Finally there's the $width and $height which specify the maximum width and height of the device.

The first string is a Status string which will be either 't' for text attachments or 'i' for image attachments.

Let's look at the text attachment first. The strings after the Status are below.

a_mode
attachment_id
filename
attachment_body

a_mode is "A", not sure if it can be anything else. attachment_id is the id of the attachment. filename is the filename of the attachment. Finally attachment_body is the body of the attachment.

Now let's look at an image attachment.

filename
null_string
attachment_file

filename is the filename of the attachment, null_string is the 00 00 empty string, this is followed by the actual attachment. In this case, attachment_file is not a string, it does not have a length. Instead it is just a big chunk of data that appears after the null_string.

If Google couldn't preview your attachment for some reason or another, it will return a text attachment with the error message in the body.

Mark Thread

Now let's look at how to mark a thread. This means either Star a thread, or mark it read or unread, or archive it. You send a request with the following POST variables.

bu=$action
t=$thread_id
at=$GMAIL_AT

$action is the action to take.
st = Star Thread
xst = Unstar Thread
ur = Mark Unread
rd = Mark Read
ar = Archive Thread
tr = Trash Thread
sp = Mark as Spam

$thread_id is the id of the thread to modify. Yes that is a "t=" and not a "th=". This is important. Finally there's $GMAIL_AT. This should simply be the GMAIL_AT cookie that gmail sent when you logged in.

Searching

If you can handle the inbox, you can handle searching. First you POST a request with:

s=$search_type
q=$query (for $search_type="q")
cat=$category (for $search_type="cat")
sz=$num_per_page (optional)
st=$start (optional)

$search_type is the type of "search". "q" is for query searches, "cat" is for category searches. Viewing your Drafts Folder is done with a category search.

$query is the search query (it is not present at all for category searches). If you prefix your query with "L:" you can do a label view. For example, if you have a label called "Work" you can view your work folder by doing a "q" search with a q=L:Work

$category is the category to view. This is not present at all for query searches. Possible categories are: "Inbox", "Starred", "Chats", "Sent Mail", "Drafts", "All Mail", "Spam", "Trash".

$sz is the number of messages per page to display. This is optional, defaulting to 50 if not present.

$st is the start offset. For example, if you have $sz=20, then when you want to view the next page of results, you would re-issue the same search command, only setting $st=20 then $st=40, and so on. This is optional, and it will default to 0 if not present.

The response from the server for doing any of these searches is identical to the packet you got for the inbox. In fact, you can include any of the above variables when you log in to affect the initial results you get on successful login.

Sending Mail

Finally, we see how to actually send mail. You send a packet with the following POST variables:

at=$GMAIL_AT
to=$to_addresses
cc=$cc_addresses
bcc=$bcc_addresses
subject=$subject
body=$body
bu=sfc
attach=$attachment (optional)

$GMAIL_AT is the GMAIL_AT cookie. $to_addresses is the comma-separated list of addresses to send mail to. $cc_addresses contains any carbon copy addresses, and $bcc_addresses contains any blind carbon copy addresses. $subject is the subjectline of the email, $body is the body of the email. $attachment contains any attachments, but I haven't figured out the format of the attachment yet.


Working Example

What fun is it without a working example? Here's a simple example in PHP that connects to gmail and prints out the first 10 items of your inbox.


<?
$user="username";
$pass="password";

$ch=curl_init();
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
curl_setopt($ch,CURLOPT_URL,
"https://mail.google.com/mail/m/".rand(1000,90000));
curl_setopt($ch,CURLOPT_HEADER,false);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_BINARYTRANSFER,true);
curl_setopt($ch,CURLOPT_POST,true);
$post="p=1.1&zym=l&user=".urlencode($user).
"&password=".urlencode($pass)."&sz=10";
curl_setopt($ch,CURLOPT_POSTFIELDS,$post);
$data=curl_exec($ch);
curl_close($ch);

$status=readUTF($data);
if ($status!='T') die("Error logging in");
readUTF($data); //id
$start=readUTF($data);
$num_msgs=readUTF($data);
$total=readUTF($data);
echo "Displaying ".($start+1)." to ".($num_msgs+$start).
" of $total<br />\n";
$num_unread=readUTF($data);
echo "$num_unread unread messages<br />\n";
echo "<table><tr><th>Flags</th><th>From</th>";
echo "<th>Subject</th><th>When</th></tr>\n";
for ($i=0;$i<$num_msgs;$i++)
{
$isread=readUTF($data);
$isstar=readUTF($data);
$hasattach=readUTF($data);
$from=readUTF($data);
$subject=readUTF($data);
$time=readUTF($data);
$url=readUTF($data);
echo "<tr><td>";
echo ($isread=='T')?"R":"r";
echo ($isstar=='T')?"*":"_";
echo ($hasattach=='T')?"A":"a";
echo "</td><td>$from</td><td>$subject</td>";
echo "<td>$time</td></tr>\n";
}
echo "</table>\n";
$numlabels=readUTF($data);
for ($i=0;$i<$numlabels;$i++)
{
$label=readUTF($data);
$num_unread=readUTF($data);
echo "$label ($num_unread)<br />\n";
}

function readUTF(&$feed)
{
$array=unpack("nlen",$feed);
$utf=substr($feed,2,$array['len']);
$feed=substr($feed,2+$array['len']);
return $utf;
}

?>

7 comments:

Per Olesen said...

Hi Sean,

Saw your comment on my blog and read the entry here. What a nice write-up of the mobile api. Thanks.

I'm done myself with my application, but I posted your blog post to dzone, as I thought others might like it.

/Per

Unknown said...
This comment has been removed by the author.
Sean said...

Curl is a little weird with cookies. I had the most success by adding these two options:

CURLOPT_COOKIEJAR and CURLOPT_COOKIEFILE

Just point them both to a random filename in /tmp/ COOKIEJAR saves cookies, COOKIEFILE sends those saved cookies back to the server.

NoddyC said...

Just to complete this a little, in response to ... "$attachment contains any attachments, but I haven't figured out the format of the attachment yet" ...

There does not seem to be a way to post a new attachment with the mobile interface.

However, it does allow mail to be forwarded with attachements intact, so in the "attach=$attachment" part of the POST to send mail, use
the attachment_id obtained from the original mail when you read it in.

Multiple attachments can be forwarded by stringing these together: "&attach=id1&attach=id2&..."

Rana said...

nice & helpfull article. I like it

Mark said...

Thanks very much for this great write up. Excuse me while I spam you with questions.

Is this still relevant?

Is there a version newer than 1.1

Is this officially documented anywhere?

Is this officially supported?

Anyway to use this with a Google Apps account (as in not @gmail.com)?

Thanks!

Sean said...

It might still be relevant if you want to star and archive mail (I don't believe you can do that over imap), but I think it needs to be updated. As far as I know, there still isn't an official mobile API. Someone would have to reverse engineer the API again.