Email remains at the heart of how companies operate. That's why earlier this year, we previewed Gmail Add-ons—a way to help businesses speed up workflows. Since then, we've seen partners build awesome applications, and beginning today, we're extending the Gmail add-on preview to include all developers. Now anyone can start building a Gmail add-on.
Gmail Add-ons let you integrate your app into Gmail and extend Gmail to handle quick actions.
They are built using native UI context cards that can include simple text dialogs, images, links, buttons and forms. The add-on appears when relevant, and the user is just a click away from your app's rich and integrated functionality.
Gmail Add-ons are easy to create. You only have to write code once for your add-on to work on both web and mobile, and you can choose from a rich palette of widgets to craft a custom UI. Create an add-on that contextually surfaces cards based on the content of a message. Check out this video to see how we created an add-on to collate email receipts and expedite expense reporting.
Per the video, you can see that there are three components to the app's core functionality. The first component is getContextualAddOn()—this is the entry point for all Gmail Add-ons where data is compiled to build the card and render it within the Gmail UI. Since the add-on is processing expense reports from email receipts in your inbox, the createExpensesCard() parses the relevant data from the message and presents them in a form so your users can confirm or update values before submitting. Finally, submitForm()takes the data and writes a new row in an "expenses" spreadsheet in Google Sheets, which you can edit and tweak, and submit for approval to your boss.
getContextualAddOn()
createExpensesCard()
submitForm()
Check out the documentation to get started with Gmail Add-ons, or if you want to see what it's like to build an add-on, go to the codelab to build ExpenseIt step-by-step. While you can't publish your add-on just yet, you can fill out this form to get notified when publishing is opened. We can't wait to see what Gmail Add-ons you build!
You might be using the Google Calendar API, or alternatively email markup, to insert events into your users' calendars. Thankfully, these tools allow your apps to do this seamlessly and automatically, which saves your users a lot of time. But what happens if plans change? You need your apps to also be able to modify an event.
While email markup does support this update, it's limited in what it can do, so in today's video, we'll show you how to modify events with the Calendar API. We'll also show you how to create repeating events. Check it out:
Imagine a potential customer being interested in your product, so you set up one or two meetings with them. As their interest grows, they request regularly-scheduled syncs as your product makes their short list—your CRM should be able to make these adjustments in your calendar without much work on your part. Similarly, a "dinner with friends" event can go from a "rain check" to a bi-monthly dining experience with friends you've grown closer to. Both of these events can be updated with a JSON request payload like what you see below to adjust the date and make it repeating:
JSON
var TIMEZONE = "America/Los_Angeles"; var EVENT = { "start": {"dateTime": "2017-07-01T19:00:00", "timeZone": TIMEZONE}, "end": {"dateTime": "2017-07-01T22:00:00", "timeZone": TIMEZONE}, "recurrence": ["RRULE:FREQ=MONTHLY;INTERVAL=2;UNTIL=20171231"] };
This event can then be updated with a single call to the Calendar API's events().patch() method, which in Python would look like the following given the request data above, GCAL as the API service endpoint, and a valid EVENT_ID to update:
events().patch()
GCAL
EVENT_ID
GCAL.events().patch(calendarId='primary', eventId=EVENT_ID, sendNotifications=True, body=EVENT).execute()
If you want to dive deeper into the code sample, check out this blog post. Also, if you missed it, check out this video that shows how you can insert events into Google Calendar as well as the official API documentation. Finally, if you have a Google Apps Script app, you can access Google Calendar programmatically with its Calendar service.
We hope you can use this information to enhance your apps to give your users an even better and timely experience.
Posted by Wesley Chun (@wescpy), Developer Advocate, G Suite
Email continues to be a dominant form of communication, personally and professionally, and our email signature serves as both a lightweight introduction and a business card. It's also a way to slip-in a sprinkling of your personality. Wouldn't it be interesting if you could automatically change your signature whenever you wanted without using the Gmail settings interface every time? That is exactly what our latest video is all about.
If your app has already created a Gmail API service endpoint, say in a variable named GMAIL, and you have the YOUR_EMAIL email address whose signature should be changed as well as the text of the new signature, updating it via the API is as pretty straightforward, as illustrated by this Python call to the GMAIL.users().settings().sendAs().patch() method:
GMAIL
YOUR_EMAIL
GMAIL.users().settings().sendAs().patch()
signature = {'signature': '"I heart cats." ~anonymous'} GMAIL.users().settings().sendAs().patch(userId='me', sendAsEmail=YOUR_EMAIL, body=signature).execute()
For more details about the code sample used in the requests above as well as in the video, check out the deepdive post. In addition to email signatures, other settings the API can modify include: filters, forwarding (addresses and auto-forwarding), IMAP and POP settings to control external email access, and the vacation responder. Be aware that while API access to most settings are available for any G Suite Gmail account, a few sensitive operations, such as modifying send-as aliases or forwarding, are restricted to users with domain-wide authority.
Developers interested in using the Gmail API to access email threads and messages instead of settings can check out this other video where we show developers how to search for threads with a minimum number of messages, say to look for the most discussed topics from a mailing list. Regardless of your use-case, you can find out more about the Gmail API in the developer documentation. If you're new to the API, we suggest you start with the overview page which can point you in the right direction!
Be sure to subscribe to the Google Developers channel and check out other episodes in the G Suite Dev Show video series.
Posted by Michael Winser, Product Lead, Google Apps and Wesley Chun, Developer Advocate, Google Apps
Last week, we clarified the expectations and responsibilities when accessing Google user data via OAuth 2.0. Today, we’re announcing that in order to better protect users, we are increasing account security for enterprise Gmail users effective October 5, 2016. At this time, a new policy will take effect whereby users in a Google Apps domain, while changing their passwords on or after this date, will result in the revocation of the OAuth 2.0 tokens of apps that access their mailboxes using Gmail-based authorization scopes. Please note that users will not notice any specific changes on this date and their applications will continue to work. It is only when a user changes their password from that point moving forward that their Gmail-related tokens become invalid.
Developers should modify their applications to handle HTTP 400 or 401 error codes resulting from revoked tokens and prompt their users to go through the OAuth flow again to re-authorize those apps, such that they can access the user’s mailbox again (additional details below). Late last year, we announced a similar, planned change to our security policy that impacted a broader set of authorization scopes. We later decided not to move forward with that change for Apps customers and began working on a less impactful update as described above.
What is a revoked token?
A revoked OAuth 2.0 token no longer provides access to a user’s resources. Any attempt to use a revoked token in API calls will result in an error. Any existing token strings will no longer have any value and should be discarded. Applications accessing Google APIs should be modified to handle failed API calls.
Token revocation itself is not a new feature. Users have always been able to revoke access to applications in Security Checkup, and Google Apps admins have the ability to do the same in the Admin console. In addition, tokens that were not used for extended periods of time have always been subject to expiration or revocation. This change in our security policy will likely increase the rate of revoked tokens that applications see, since in some cases the process will now take place automatically.
What APIs and scopes are impacted?
To achieve the security benefits of this policy change with minimal admin confusion and end-user disruption, we’ve decided to limit its application to mail scopes only and to exclude Apps Script tokens. Apps installed via the Google Apps Marketplace are also not subject to the token revocation. Once this change is in effect, third-party mail apps like Apple Mail and Thunderbird―as well as other applications that use multiple scopes that include at least one mail scope―will stop accessing data upon password reset until a new OAuth 2.0 token has been granted. Your application will need to detect this scenario, notify the user that your application has lost access to their account data, and prompt them to go through the OAuth 2.0 flow again.
Mobile mail applications are also included in this policy change. For example, users who use the native mail application on iOS will have to re-authorize with their Google account credentials when their password has been changed. This new behavior for third-party mail apps on mobile aligns with the current behavior of the Gmail apps on iOS and Android, which also require re-authorization upon password reset.
How can I determine if my token was revoked?
Both short-lived access tokens and long-lived refresh tokens will be revoked when a user changes their password. Using a revoked access token to access an API or to generate a new access token will result in either HTTP 400 or 401 errors. If your application uses a library to access the API or handle the OAuth flow, then these errors will likely be thrown as exceptions. Consult the library’s documentation for information on how to catch these exceptions. NOTE: because HTTP 400 errors may be caused by a variety of reasons, expect the payload from a 400 due to a revoked token to be similar to the following:
{ "error_description": "Token has been revoked.", "error": "invalid_grant" }
How should my application handle revoked tokens?
This change emphasizes that token revocation should be considered a normal condition, not an error scenario. Your application should expect and detect the condition, and your UI should be optimized for restoring tokens.
To ensure that your application works correctly, we recommend doing the following:
If your application uses incremental authorization to accrue multiple scopes in the same token, you should track which features and scopes a given user has enabled. The end result is that if your app requested and obtained authorization for multiple scopes, and at least one of them is a mail scope, that token will be revoked, meaning you will need to prompt your user to re-authorize for all scopes originally granted.
Many applications use tokens to perform background or server-to-server API calls. Users expect this background activity to continue reliably. Since this policy change also affects those apps, this makes prompt notification requesting re-authorization even more important.
What is the timeline for this change?
To summarize, properly configured applications should be expected to handle invalid tokens in general, whether they be from expiration, non-existence, and revocation as normal conditions. We encourage developers to make any necessary changes to give their users the best experience possible. The policy change is planned to take effect on October 5, 2016.
Please see this Help Center article and FAQ for more details and the full list of mail scopes. Moving forward, any additional scopes to be added to the policy will be communicated in advance. We will provide those details as they become available.
-webkit-transition-property
-webkit-transition-duration
-webkit-transition-timing-function
-webkit-transition-delay
-webkit-transition-timing-function: ease-in-out
-webkit-transition
-webkit-transform
translate3d
rotate
-webkit-transform: translate(100px, 0) rotate(45deg);
top
left
translate(x, y)
card.style.WebkitTransform =
‘translate3d(-700px, 0, 0) rotate(5deg)’;
window.setTimeout(function() { card.style.WebkitTransition = ‘-webkit-transform 300ms ease-in-out’; card.style.WebkitTransform = ‘translate3d(0, 0, 0) rotate(5deg)’; }, 0);
element.addEventListener(‘webkitTransitionEnd’, listener, false);
card.style.WebkitTransform = ‘translate3d(-700px, 0, 0) rotate(5deg)’;
translated3d(-350px, 0, 0) rotate(5deg).
translate3d(-700px, 0, 0) rotate(5deg).
.CSS_FLOATY_BAR { ... top: -50px; /* start off the screen, so it slides in nicely */ -webkit-transition: top 0.2s ease-out; ...}
// Constructor for the floaty bargmail.FloatyBar = function() { this.menuDiv = document.createElement('div'); this.menuDiv.className = CSS_FLOATY_BAR; ...};// Called when it's time for the floaty bar to movegmail.FloatyBar.prototype.setTop = function() { this.menuDiv.style.top = window.scrollY + 'px';};// Called when the floaty bar menu is dismissedgmail.FloatyBar.prototype.hideOffScreen = function() { this.menuDiv.style.top = '-50px';};gmail.floatyBar = new gmail.FloatyBar();// Listen for scroll events on the top level windowwindow.onscroll = function() { ... gmail.floatyBar.setTop(); ...};
.CSS_FLOATY_BAR { ... top: -50px; /* start off the screen, so it slides in nicely */ -webkit-transition: -webkit-transform 0.2s ease-out; ...}
// Called when it's time for the floaty bar to movegmail.FloatyBar.prototype.setTop = function() { var translate = window.scrollY - (-50); this.menuDiv.style['-webkit-transform'] = 'translateY(' + translate + 'px)';};// Called when the floaty bar menu is dismissedgmail.FloatyBar.prototype.hideOffScreen = function() { this.menuDiv.style['-webkit-transform'] = 'translateY(0px)';};
function grow() { var textarea = document.getElementById('growingTextarea'); var newHeight = textarea.scrollHeight; var currentHeight = textarea.clientHeight;…}
if (newHeight > currentHeight) { textarea.style.height = newHeight + 5 * TEXTAREA_LINE_HEIGHT + 'px';}
<script>// Value of the line-height CSS property for the textarea.var TEXTAREA_LINE_HEIGHT = 13;function grow() { var textarea = document.getElementById('growingTextarea'); var newHeight = textarea.scrollHeight; var currentHeight = textarea.clientHeight; if (newHeight > currentHeight) { textarea.style.height = newHeight + 5 * TEXTAREA_LINE_HEIGHT + 'px'; }}</script><textarea id="growingTextarea" onkeyup="grow();"></textarea>
SELECT Author, COUNT(*) as NumArticlesFROM MagazinesGROUP BY AuthorORDER BY NumArticles;SELECT Author, COUNT(*) as NumBooksFROM BooksGROUP BY AuthorORDER BY NumBooks;
SELECT Author, NumPublications, PubTypeFROM ( SELECT Author, COUNT(*) as NumPublications, 'Magazine' as PubType, 0 as SortIndex FROM Magazines GROUP BY Author UNION SELECT Author, COUNT(*) as NumPublications, 'Book' as PubType, 1 as SortIndex FROM Books GROUP BY Author)ORDER BY SortIndex, NumPublications;
CREATE TRIGGER IF NOT EXISTS RemoveAuthorAFTER DELETE ON BooksBEGIN DELETE FROM Authors WHERE Author NOT IN (SELECT Author FROM Books);END;
google.wspl.Statement
google.wspl.Transaction
google.wspl.ResultSet
google.wspl.Database
google.wspl.DatabaseFactory
var database = google.wspl.DatabaseFactory.createDatabase('db name', 'http://yourdomain/dbworker.js');
var statement = google.wspl.Statement('SELECT col from test_table;');database.createTransaction(function(tx) { tx.executeAll([statement], {onSuccess: function(tx, resultSet) { // Statement succeeded. for(; resultSet.isValidRow(); resultSet.next()) { window.console.info(resultSet.getRow()['col']); } }, onFailure: function(error) { // Statement failed. }}); }, {onSuccess: function() { // After transaction commits, before any other starts.}, onFailure: function(error) { // After transaction fails, before any other starts.}});
~/Library/Application Support/iPhone Simulator/User/Library/Caches/com.apple.WebAppCache/ApplicationCache.db
$ sqlite3 ApplicationCache.dbSQLite version 3.4.0Enter ".help" for instructionssqlite> .mode columnsqlite> .headers onsqlite> .tablesCacheEntries CacheResourceData CacheWhitelistURLs FallbackURLs CacheGroups CacheResources Caches SchemaVersion sqlite> select * from CacheGroups;id manifestHostHash manifestURL newestCache---------- ---------------- ------------------------------------------------- -----------1 906983082 http://mail.google.com/mail/s/?v=ma&name=sm 1
sqlite> select * from Caches;id cacheGroup---------- ----------1 1
sqlite> select * from CacheEntries limit 1;cache type resource ---------- ---------- ----------1 2 1
sqlite> select * from CacheResources where id=1;id url statusCode responseURL ---------- ------------------------------------------- ---------- -----------------------1 http://mail.google.com/mail/s/?v=ma&name=sm 200 http://mail.google.c...mimeType textEncodingName headers data------------------- ---------------- --------------text/cache-manifest utf-8 sqlite> .schema CacheResourceDataCREATE TABLE CacheResourceData (id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB);
sqlite> select type,url,mimeType,statusCode from CacheEntries,CacheResources where resource=id;type url mimeType statusCode---------- ---------------------------------------------- ------------------- ----------2 http://mail.google.com/mail/s/?v=ma&name=sm text/cache-manifest 200 4 http://mail.google.com/mail/images/xls.gif image/gif 200 4 http://mail.google.com/mail/images/pdf.gif image/gif 200 4 http://mail.google.com/mail/images/ppt.gif image/gif 200 4 http://mail.google.com/mail/images/sound.gif image/gif 200 4 http://mail.google.com/mail/images/doc.gif image/gif 200 4 http://mail.google.com/mail/images/graphic.gif image/gif 200 1 http://mail.google.com/mail/s text/html 200 4 http://mail.google.com/mail/images/generic.gif image/gif 200 4 http://mail.google.com/mail/images/zip.gif image/gif 200 4 http://mail.google.com/mail/images/html2.gif image/gif 200 4 http://mail.google.com/mail/images/txt.gif image/gif 200
CACHE MANIFEST# version: 3f1b9s84jsfile1.js... other URLs ...
CACHE MANIFESTjsfile1.jsNETWORK:/images/FALLBACK:/thumbnails/ images/missing_thumb.jpg
if (window.applicationCache.status == 0) { // Page was loaded from the Network.} else { // Page was loaded from AppCache}
CACHE MANIFESTjsfile1.jsjsfile2.jsstyles.css/images/image1.png/images/image2.png
<html manifest="/sitemanifest">