7

I'm trying to send a new email through the gmail package . However the Message type which is required by the send method is poorly documented. Most of the fields seem used to actually parse/read emails. The only field which makes sense (at some degree) for the send method is Payload of type MessagePart though I can't figure it out how to generate the MessagePartBody as it seems to be a kind of mime type. Below is the code I have so far.

func (em *Email) SendMessage(cl *Client) error {
    config.ClientId = cl.Username
    config.ClientSecret = cl.Password

    t := &oauth.Transport{

        Config:    config,
        Transport: http.DefaultTransport,
    }
    var tk oauth.Token
    err := json.Unmarshal([]byte(cl.Meta), &tk)
    t.Token = &tk
    if err != nil {
        log.Errorf("meta %v, err %v", cl.Meta, err)
        return err
    }
    gmailService, err := gmail.New(t.Client())
    if err != nil {
        log.Error(err)
        return err
    }

    p := gmail.MessagePart{}
    p.Headers = append(p.Headers, &gmail.MessagePartHeader{
        Name:  "From",
        Value: em.FromEmail,
    })
    p.Headers = append(p.Headers, &gmail.MessagePartHeader{
        Name:  "To",
        Value: em.ToEmail,
    })
    p.Headers = append(p.Headers, &gmail.MessagePartHeader{
        Name:  "Subject",
        Value: em.Subject,
    })

    emsg := base64.StdEncoding.EncodeToString(em.Message)
    log.Info(emsg)
    msg := gmail.Message{
        Payload: &p,
        Raw:     "",
    }
    _, err = gmailService.Users.Messages.Send("me", &msg).Do()
    if err != nil {
        log.Error(err)
        return err
    }
    return err
}

The "REST" API is even more confusing. It requires an uploadType param (WTF to upload) and a raw field which I guess is the raw message which requires a format provided by messages.get. Why would you send a message from your inbox which literally would be a 'resend' as your are on the receipt list ? Am I the only one who thinks this API(or at least the documentation) is just crap ?

hey
  • 7,299
  • 14
  • 39
  • 57
  • `MessagePartBody` is a simple struct, which contains `Data string` and `Size int64`. What exactly is the problem when you try to build a complete message and send it? – JimB Aug 19 '14 at 13:51
  • The send method panics if you assign a simple string to data and the length of ``[]byte(Data)``. – hey Aug 19 '14 at 14:03
  • What's the panic message? (and you don't need to copy the string into a []byte to get the length) – JimB Aug 19 '14 at 14:07
  • The error is ``invalid memory address or nil pointer dereference``. I assume the ``data`` is not a simple string and is actually parsed somewhere thus the reason of panic – hey Aug 19 '14 at 14:10
  • No, that means you're dereferencing a nil pointer; the stack trace will show you where. – JimB Aug 19 '14 at 14:12
  • Indeed... you was right.. still I get this error ``RFC822 payload message string or uploading message via /upload/* URL required, invalidArgument`` – hey Aug 19 '14 at 14:25
  • I guess ``Data`` is not a simple string after all. – hey Aug 19 '14 at 14:42

2 Answers2

6

It was a bit tricky but here is how you can send emails through the GMAIL API

import(
    "code.google.com/p/goauth2/oauth"
    "code.google.com/p/google-api-go-client/gmail/v1"
    log "github.com/golang/glog"

    "encoding/base64"
    "encoding/json"
    "net/mail"
    "strings"
    )


type Email struct {
    FromName, FromEmail, ToName, ToEmail, Subject string
    Message                                       string
}

func (em *Email) SendMessage(cl *Client) error {
    config.ClientId = cl.Username //oauth clientID
    config.ClientSecret = cl.Password  //oauth client secret 

    t := &oauth.Transport{
        Config:    config,
        Transport: http.DefaultTransport,
    }
    var tk oauth.Token
    err := json.Unmarshal([]byte(cl.Meta), &tk)
    t.Token = &tk
    if err != nil {
        log.Errorf("meta %v, err %v", cl.Meta, err)
        return err
    }
    gmailService, err := gmail.New(t.Client())
    if err != nil {
        log.Error(err)
        return err
    }

    from := mail.Address{em.FromName, em.FromEmail}
    to := mail.Address{em.ToName, em.ToEmail}

    header := make(map[string]string)
    header["From"] = from.String()
    header["To"] = to.String()
    header["Subject"] = encodeRFC2047(em.Subject)
    header["MIME-Version"] = "1.0"
    header["Content-Type"] = "text/html; charset=\"utf-8\""
    header["Content-Transfer-Encoding"] = "base64"

    var msg string
    for k, v := range header {
        msg += fmt.Sprintf("%s: %s\r\n", k, v)
    }
    msg += "\r\n" + em.Message

    gmsg := gmail.Message{
        Raw: encodeWeb64String([]byte(msg)),
    }

    _, err = gmailService.Users.Messages.Send("me", &gmsg).Do()
    if err != nil {
        log.Errorf("em %v, err %v", gmsg, err)
        return err
    }
    return err
}



func encodeRFC2047(s string) string {
    // use mail's rfc2047 to encode any string
    addr := mail.Address{s, ""}
    return strings.Trim(addr.String(), " <>")
}

func encodeWeb64String(b []byte) string {

    s := base64.URLEncoding.EncodeToString(b)

    var i = len(s) - 1
    for s[i] == '=' {
        i--
    }

    return s[0 : i+1]
}
hey
  • 7,299
  • 14
  • 39
  • 57
  • text/html or any other random email types all work fine. The trick is simply composing a valid message in MIME format, which may be either easy or very hard depending on the email libraries that exist for your language. Either way, currently the gmail API only accepts the full email string (e.g. the thing that would be sent at "DATA" time in SMTP) currently. – Eric D Aug 19 '14 at 17:00
  • I've actually tried to send the payload used to send email through SMTP but for some reasons the Gmail API validation rejects it. See the updated answer with the mime payload. – hey Aug 19 '14 at 17:41
  • 3
    @EricDeFriez I finally fixed it... the issue was due the base64encoding (had to be ``web safe``) . Still I believe the go SDK is quite a mess. You shouldn't go through RFCs to send an email. That should be handled by the SDK. – hey Aug 19 '14 at 18:15
1

Similar to @hey 's answer, but I tidied it up, and allowed the email to put newlines in the email body through \n and show up correctly on the email client. Also, @hey is not using the new supported Gmail API. Here is the final code:

import (
    "encoding/base64"
    "golang.org/x/net/context"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
    "google.golang.org/api/gmail/v1"
    "encoding/json"
    "net/mail"
)

type Email struct {
    FromName  string
    FromEmail string
    ToName    string
    ToEmail   string
    Subject   string
    Message   string
}


func (em *Email) sendMailFromEmail() error {
b, err := ioutil.ReadFile("credentials.json")
if err != nil {
    log.Fatalf("Unable to read client secret file: %v", err)
}

// If modifying these scopes, delete your previously saved token.json.
config, err := google.ConfigFromJSON(b, gmail.GmailSendScope)
if err != nil {
    log.Fatalf("Unable to parse client secret file to config: %v", err)
}
cl := getClientMail(config)

gmailService, err := gmail.New(cl)
if err != nil {
    log.Fatalf("Unable to retrieve Gmail client: %v", err)
}

from := mail.Address{em.FromName, em.FromEmail}
to := mail.Address{em.ToName, em.ToEmail}

header := make(map[string]string)
header["From"] = from.String()
header["To"] = to.String()
header["Subject"] = em.Subject
header["MIME-Version"] = "1.0"
header["Content-Type"] = "text/plain; charset=\"utf-8\""
header["Content-Transfer-Encoding"] = "base64"

var msg string
for k, v := range header {
    msg += fmt.Sprintf("%s: %s\r\n", k, v)
}
msg += "\r\n" + em.Message

gmsg := gmail.Message{
    Raw: base64.RawURLEncoding.EncodeToString([]byte(msg)),
}

_, err = gmailService.Users.Messages.Send("me", &gmsg).Do()
if err != nil {
    log.Printf("em %v, err %v", gmsg, err)
    return err
}
return err
}

I did not include the following functions: getClient, getTokenFromWeb, tokenFromFile, and saveToken. You can find them, and learn how to enable the Gmail API through this tutorial by Google.

Gabe
  • 5,643
  • 3
  • 26
  • 54