Golang : GMail API create and send draft with simple upload attachment example

Continuing from our previous example of how to send an email with attachment using Gmail API. We will now explore how to do a simple upload of attachment to a draft email and send the draft email out with Gmail API.

Before you start, please turn on the Gmail API if you haven't do so and download the credential file - client_secret.json by following the Step 1: Turn on the Gmail API section found in https://developers.google.com/gmail/api/quickstart/go#prerequisites

Once you've downloaded the credential file, move it to the same directory as the source code below before running the program.


You will need to change the email send and recipients address. Also change the img.pdf file to be attached to something else that you have.

When prompted for the authorization code for the first time, cut-n-paste the URL in your browser, then you will get a string(token), cut-n-paste that string into your terminal where you execute the program.

In case you get Authorization error in the API JSON reply, you will need to authorize the Gmail API v1 scope at https://developers.google.com/oauthplayground/

Here you go!

 package main

 import (

 // NOTE : we don't want to visit CSRF URL to get the authorization code
 // and paste into the terminal each time we want to send an email
 // therefore we will retrieve a token for our client, save the token into a file
 // you will be prompted to visit a link in your browser for authorization code only ONCE
 // and subsequent execution of the program will not prompt you for authorization code again
 // until the token expires.

 // getClient uses a Context and Config to retrieve a Token
 // then generate a Client. It returns the generated Client.
 func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
 cacheFile, err := tokenCacheFile()
 if err != nil {
 log.Fatalf("Unable to get path to cached credential file. %v", err)
 tok, err := tokenFromFile(cacheFile)
 if err != nil {
 tok = getTokenFromWeb(config)
 saveToken(cacheFile, tok)
 return config.Client(ctx, tok)

 // getTokenFromWeb uses Config to request a Token.
 // It returns the retrieved Token.
 func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
 authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
 fmt.Printf("Go to the following link in your browser then type the "+
 "authorization code: \n%v\n", authURL)

 var code string
 if _, err := fmt.Scan(&code); err != nil {
 log.Fatalf("Unable to read authorization code %v", err)

 tok, err := config.Exchange(oauth2.NoContext, code)
 if err != nil {
 log.Fatalf("Unable to retrieve token from web %v", err)
 return tok

 // tokenCacheFile generates credential file path/filename.
 // It returns the generated credential path/filename.
 func tokenCacheFile() (string, error) {
 usr, err := user.Current()
 if err != nil {
 return "", err
 tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
 os.MkdirAll(tokenCacheDir, 0700)
 return filepath.Join(tokenCacheDir,
 url.QueryEscape("gmail-go-sendemail.json")), err

 // tokenFromFile retrieves a Token from a given file path.
 // It returns the retrieved Token and any read error encountered.
 func tokenFromFile(file string) (*oauth2.Token, error) {
 f, err := os.Open(file)
 if err != nil {
 return nil, err
 t := &oauth2.Token{}
 err = json.NewDecoder(f).Decode(t)
 defer f.Close()
 return t, err

 // saveToken uses a file path to create a file and store the
 // token in it.
 func saveToken(file string, token *oauth2.Token) {
 fmt.Printf("Saving credential file to: %s\n", file)
 f, err := os.Create(file)
 if err != nil {
 log.Fatalf("Unable to cache oauth token: %v", err)
 defer f.Close()

 func chunkSplit(body string, limit int, end string) string {

 var charSlice []rune

 // push characters to slice
 for _, char := range body {
 charSlice = append(charSlice, char)

 var result string = ""

 for len(charSlice) >= 1 {
 // convert slice/array back to string
 // but insert end at specified limit

 result = result + string(charSlice[:limit]) + end

 // discard the elements that were copied over to result
 charSlice = charSlice[limit:]

 // change the limit
 // to cater for the last few words in
 // charSlice
 if len(charSlice) < limit {
 limit = len(charSlice)


 return result


 func randStr(strSize int, randType string) string {

 var dictionary string

 if randType == "alphanum" {
 dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

 if randType == "alpha" {
 dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

 if randType == "number" {
 dictionary = "0123456789"

 var bytes = make([]byte, strSize)
 for k, v := range bytes {
 bytes[k] = dictionary[v%byte(len(dictionary))]
 return string(bytes)

 func main() {
 ctx := context.Background()

 // process the credential file
 credential, err := ioutil.ReadFile("client_secret.json")
 if err != nil {
 log.Fatalf("Unable to read client secret file: %v", err)

 // In order for POST upload attachment to work
 // You need to authorize the Gmail API v1 scope
 // at https://developers.google.com/oauthplayground/
 // otherwise you will get Authorization error in the API JSON reply

 // Use MailGoogleComScope for this example. Because of Upload Attachments
 // and Draft creation.  see https://developers.google.com/gmail/api/auth/scopes

 // See the rest at https://godoc.org/google.golang.org/api/gmail/v1#pkg-constants

 config, err := google.ConfigFromJSON(credential, gmail.MailGoogleComScope)
 if err != nil {
 log.Fatalf("Unable to parse client secret file to config: %v", err)

 client := getClient(ctx, config)

 // initiate a new gmail client service
 gmailClientService, err := gmail.New(client)
 if err != nil {
 log.Fatalf("Unable to initiate new gmail client: %v", err)

 // get our token
 cacheFile, err := tokenCacheFile()
 if err != nil {
 log.Fatalf("Unable to get path to cached credential file. %v", err)

 token, err := tokenFromFile(cacheFile)
 if err != nil {
 log.Fatalf("Unable to get token from file. %v", err)

 // read file for attachment purpose
 fileName := "img.pdf"
 fileBytes, err := ioutil.ReadFile(fileName)
 if err != nil {
 log.Fatalf("Unable to read file for attachment: %v", err)

 fileMIMEType := http.DetectContentType(fileBytes)

 fileData := base64.StdEncoding.EncodeToString(fileBytes)

 fileSize := len(string(fileBytes))

 content := `Hello!
 this is a draft message with attachment.

 userID := "me"

 postURL := "https://www.googleapis.com/upload/gmail/v1/users/" + userID + "/drafts?uploadType=media"

 // extract auth or access token from Token file
 // see https://godoc.org/golang.org/x/oauth2#Token
 authToken := token.AccessToken

 boundary := randStr(32, "alphanum")

 emailMessageData := []byte("Content-Type: multipart/mixed; boundary=" + boundary + " \n" +
 "MIME-Version: 1.0\n" +
 "to: " + "[REPLACE]@gmail.com" + "\n" +
 "from: " + "[REPLACE]@gmail.com" + "\n" +
 "subject: " + "upload attachment to draft and send" + "\n\n" +

 "--" + boundary + "\n" +
 "Content-Type: text/plain; charset=" + string('"') + "UTF-8" + string('"') + "\n" +
 "MIME-Version: 1.0\n" +
 "Content-Transfer-Encoding: 7bit\n\n" +
 content + "\n\n" +
 "--" + boundary + "\n" +

 "Content-Type: " + fileMIMEType + "; name=" + string('"') + fileName + string('"') + " \n" +
 "MIME-Version: 1.0\n" +
 "Content-Transfer-Encoding: base64\n" +
 "Content-Disposition: attachment; filename=" + string('"') + fileName + string('"') + " \n\n" +
 chunkSplit(fileData, 76, "\n") +
 "--" + boundary + "--")

 // convert []byte to io.Reader type with bytes.NewBuffer
 // request, _ := http.NewRequest("POST", postURL, bytes.NewBuffer(emailMessageData))

 // or

 // convert []byte to io.Reader type with strings.NewReader
 // see https://www.socketloop.com/tutorials/golang-convert-cast-byte-to-io-reader-type
 request, _ := http.NewRequest("POST", postURL, strings.NewReader(string(emailMessageData)))

 // see https://www.socketloop.com/tutorials/golang-post-data-with-url-values
 request.Header.Add("Host", "www.googleapis.com")
 request.Header.Add("Content-Type", "message/rfc822")
 request.Header.Add("Content-Length", strconv.Itoa(fileSize))
 request.Header.Add("Authorization", "Bearer "+authToken)

 response, err := client.Do(request)
 if err != nil {
 log.Fatalf("Unable to be post to Google API: %v", err)

 defer response.Body.Close()
 body, err := ioutil.ReadAll(response.Body)

 if err != nil {
 log.Fatalf("Unable to read Google API response: %v", err)

 // output the response from GMail API

 // we need to extract the draft message ID to execute Send command
 jsonAPIreply, _ := jason.NewObjectFromBytes(body)

 draftID, _ := jsonAPIreply.GetString("id")
 fmt.Println("Draft ID : ", draftID)

 // ----- comment the lines below and you will send that the draft will appear 
 // ----- in your Gmail's Draft box instead of being send out

 var draft gmail.Draft
 draft.Id = draftID

 // send out our draft
 _, err = gmailClientService.Users.Drafts.Send(userID, &draft).Do()
 if err != nil {
 log.Fatalf("Unable to send message: %v", err)
 } else {
 log.Println("Draft email with ID " + draftID + " sent!")


Sample output:


"id": "r-5952729610967649299",

"message": {

"id": "155bed1af60c65db",

"threadId": "155bed1af60c65db",

"labelIds": [





Draft ID : r-5952729610967649299

2016/07/06 14:07:41 Draft email with ID r-5952729610967649299 sent!

Check the Sent box of your sender gmail account and the Inbox of the receiver account. The copies should be there if everything goes smoothly.

In this example, we only explore simple upload attachment and if you need to upload bigger attachment, you will need to use multipart or resumable upload methods.

Happy coding!








  See also : Golang : Send email with attachment(RFC2822) using Gmail API example

By Adam Ng

IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.