The following code outlines the contents of the gmail.go
file that's available in the client
package of the repository for this book. If you want to jump straight to trying out this functionality, then simply copy your credentials.json
file to the current directory and skip to the next section, Update an example to use Gmail.
We start by adding the necessary OAuth2 setup and token storage by copying the getClient()
, getTokenFromWeb()
, tokenFromFile()
, and saveToken()
functions from Google's Gmail quickstart Go file at github.com/gsuitedevs/go-samples/blob/master/gmail/quickstart/quickstart.go. These are very similar to the OAuth2 code that was created before but works better with the Google libraries.
Next, we need to set up the client from the credentials file that has been saved (in the current directory). We add a new function to parse the data, set up the authentication, and configure *gmail.Service
using the following code:
func setupService() *gmail.Service { b, err := ioutil.ReadFile("credentials.json") if err != nil { log.Fatalf("Unable to read client secret file: %v", err) } config, err := google.ConfigFromJSON(b, gmail.GmailReadonlyScope, gmail.GmailComposeScope) if err != nil { log.Fatalf("Unable to parse client secret file to config: %v", err) } client := getClient(config) srv, err := gmail.New(client) if err != nil { log.Fatalf("Unable to retrieve Gmail client: %v", err) } return srv }
The service returned from this function will be used for each subsequent call to the Gmail API as it contains the authentication configuration and credentials. Next, we need to prepare the email list by downloading all of the messages in the user's inbox. The INBOX
LabelID is used to filter messages that haven't been archived. This function requests the message list and iterates through the metadata to initiate the full download of each message. For a full implementation, we would need to add paging support (the response contains nextPageToken
, which indicates when more data is available), but this example will handle up to 100 messages:
func downloadMessages(srv *gmail.Service) { req := srv.Users.Messages.List(user) req.LabelIds("INBOX") resp, err := req.Do() if err != nil { log.Fatalf("Unable to retrieve Inbox items: %v", err) } var emails []*EmailMessage for _, message := range resp.Messages { email := downloadMessage(srv, message) emails = append(emails, email) } }
To download each individual message, we need to implement the downloadMessage()
function referenced previously. For the specified message, we download the full content using the Gmail Go API. From the resulting data, we extract the information we need from the message headers. As well as parsing the Date
header, we need to decode the message body, which is in a serialized, Base64 encoded format:
func downloadMessage(srv *gmail.Service, message *gmail.Message) *EmailMessage { mail, err := srv.Users.Messages.Get(user, message.Id).Do() if err != nil { log.Fatalf("Unable to retrieve message payload: %v", err) } var subject string var to, from Email var date time.Time content := decodeBody(mail.Payload) for _, header := range mail.Payload.Headers { switch header.Name { case "Subject": subject = header.Value case "To": to = Email(header.Value) case "From": from = Email(header.Value) case "Date": value := strings.Replace(header.Value, "(UTC)", "", -1) date, err = time.Parse("Mon, _2 Jan 2006 15:04:05 -0700", strings.TrimSpace(value)) if err != nil { log.Println("Error: Could not parse date", value) date = time.Now() } else { log.Println("date", header.Value) } } } return NewMessage(subject, content, to, from, date) }
The decodeBody()
function is as shown in the following. For plain text emails, the content is in the Body.Data
field. For multi-part messages (where the body is empty), we access the first of the multiple parts and decode that instead. Decoding the Base64 content is handled by the standard library decoder:
func decodeBody(payload *gmail.MessagePart) string { data := payload.Body.Data if data == "" { data = payload.Parts[0].Body.Data } content, err := base64.StdEncoding.DecodeString(data) if err != nil { fmt.Println("Failed to decode body", err) } return string(content) }
The final step in preparing this code is to complete the EmailServer
interface methods. The ListMessages()
function will return the result of downloadMessages()
, and we can set up CurrentMessage()
to return the email at the top of the list. Full implementation is in this book's code repository.
To send a message, we have to package up the data in a raw format to send through the API. We'll re-use the ToGMailEncoding()
function from the Post
example in Chapter 12, Concurrency, Networking, and Cloud Services. Before encoding the email, we set an appropriate "From" email address (be sure to use the email address of the account you are signed in with or a registered alias) and the current date for the time of sending. After encoding, we set the data to the Raw
field of a gmail.Message
type and pass it to the Gmail Send()
function:
func (g *gMailServer) Send(email *EmailMessage) { email.From = "YOUR EMAIL ADDRESS" email.Date = time.Now() data := email.ToGMailEncoding() msg := &gmail.Message{Raw:data} srv.Users.Messages.Send(user, msg).Do() }
This minimal code will be enough to implement sending a message. All of the hard work has been done by the earlier setup code—which provided the srv
object.
Although Google provides the ability to use push messaging, the setup is very complicated—so instead, we'll poll for new messages. Every 10 seconds, we should download any new messages that have arrived. To do this, we can use the history API, which returns any messages that appeared after a specific point in history (set using StartHistoryId()
). HistoryId
is a chronological number that marks the order that messages arrived in. Before we can use the history API, we need to have a valid HistoryId
—we can do this by adding the following line to the downloadMessage()
function:
g.history = uint64(math.Max(float64(g.history), float64(mail.HistoryId)))
Once we have a point in history to query from, we need a new function that can download any messages since this point in time. The following code is similar to downloadMessages()
in the preceding code but will only download new messages:
func (g *gMailServer) downloadNewMessages(srv *gmail.Service) []*EmailMessage{ req := srv.Users.History.List(g.user) req.StartHistoryId(g.history) req.LabelId("INBOX") resp, err := req.Do() if err != nil { log.Fatalf("Unable to retrieve Inbox items: %v", err) } var emails []*EmailMessage for _, history := range resp.History { for _, message := range history.Messages { email := downloadMessage(srv, message) emails = append(emails, email) } } return emails }
To complete the functionality, we update our Incoming()
method so that it sets up the channel and starts a thread to poll for new messages. Every 10
seconds, we'll download any new messages that have appeared and pass each to the in
channel that was created:
func (g *gMailServer) Incoming() chan *EmailMessage { in := make(chan *EmailMessage) go func() { for { time.Sleep(10 * time.Second) for _, email := range downloadNewMessages(srv) { g.emails = append([]*EmailMessage{email}, g.emails...) in <- email } } }() return in }
The complete code can be found in the client
package of this book's code repository. Let's look at how to use this new email server in our previous examples.