How to send email through Gmail Go SDK? - go

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 ?

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]
}

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.

Related

How to prompt for user password when running my code?

Hope everyone is having a good week. I’m trying to get my code to prompt a user to enter their password. as of right now if you run the code and do --help you get a few flag parameters. So I don’t want the user to show their password via the terminal prompt. Basically give the pram and fire the code and enter password. I hope I explained it right, on what I’m trying to do. I’m still green in Go. example:
-ipAddress string ip address of the managed server to be added
-password string hmc user password
-url string hmc REST api url
-user string hmc user
package main
import (
"bytes"
"crypto/tls"
"encoding/xml"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"text/template"
"golang.org/x/crypto/ssh/terminal"
)
//
// XML parsing structures
//
type Feed struct {
Entry struct {
Content struct {
ManagementConsole struct {
Metadata struct {
Atom struct {
AtomID string `xml:"AtomID"`
AtomCreated string `xml:"AtomCreated"`
} `xml:"Atom"`
} `xml:"Metadata"`
} `xml:"ManagementConsole"`
} `xml:"content"`
} `xml:"entry"`
}
// HTTP session struct
//
type Session struct {
client *http.Client
User string
Password string
url string
}
type Manage struct {
Ipaddress string
}
func NewSession(user string, password string, url string) *Session {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
jar, err := cookiejar.New(nil)
if err != nil {
log.Fatal(err)
}
return &Session{client: &http.Client{Transport: tr, Jar: jar}, User: user, Password: password, url: url}
}
func (s *Session) doLogon() {
authurl := s.url + "/rest/api/web/Logon"
// template for login request
logintemplate := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<LogonRequest xmlns="http://www.ibm.com/xmlns/systems/power/firmware/web/mc/2012_10/" schemaVersion="V1_1_0">
<Metadata>
<Atom/>
</Metadata>
<UserID kb="CUR" kxe="false">{{.User}}</UserID>
<Password kb="CUR" kxe="false">{{.Password}}</Password>
</LogonRequest>`
tmpl := template.New("logintemplate")
tmpl.Parse(logintemplate)
authrequest := new(bytes.Buffer)
err := tmpl.Execute(authrequest, s)
if err != nil {
log.Fatal(err)
}
request, err := http.NewRequest("PUT", authurl, authrequest)
// set request headers
request.Header.Set("Content-Type", "application/vnd.ibm.powervm.web+xml; type=LogonRequest")
request.Header.Set("Accept", "application/vnd.ibm.powervm.web+xml; type=LogonResponse")
request.Header.Set("X-Audit-Memento", "hmctest")
response, err := s.client.Do(request)
fmt.Println("\n")
fmt.Println(response)
if err != nil {
log.Fatal(err)
} else {
defer response.Body.Close()
if response.StatusCode != 200 {
log.Fatalf("Error status code: %d", response.StatusCode)
}
}
}
func (s *Session) getManaged()(string) {
hmcUUID :=""
// mgdurl := s.url + "/rest/api/uom/LogicalPartition"
mgdurl := s.url + "/rest/api/uom/ManagementConsole"
request, err := http.NewRequest("GET", mgdurl, nil)
request.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
response, err := s.client.Do(request)
if err != nil {
log.Fatal(err)
} else {
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
if response.StatusCode != 200 {
log.Fatalf("Error getting LPAR informations. status code: %d", response.StatusCode)
}
var feed Feed
new_err := xml.Unmarshal(contents, &feed)
if new_err != nil {
log.Fatal(new_err)
}
fmt.Printf("AtomID: %v\n", feed.Entry.Content.ManagementConsole.Metadata.Atom.AtomID)
fmt.Printf("AtomCreated: %v\n", feed.Entry.Content.ManagementConsole.Metadata.Atom.AtomCreated)
hmcUUID = feed.Entry.Content.ManagementConsole.Metadata.Atom.AtomID
}
return hmcUUID
}
func (s *Session) addManaged(uuid *string, i *Manage){
addManagedURl := s.url + "/rest/api/uom/ManagementConsole/" + *uuid + "/do/AddManagedSystem"
addManagedTemplate := `<JobRequest:JobRequest
xmlns:JobRequest="http://www.ibm.com/xmlns/systems/power/firmware/web/mc/2012_10/"
xmlns="http://www.ibm.com/xmlns/systems/power/firmware/web/mc/2012_10/"
xmlns:ns2="http://www.w3.org/XML/1998/namespace/k2" schemaVersion="V1_0">
<Metadata>
<Atom/>
</Metadata>
<RequestedOperation kb="CUR" kxe="false" schemaVersion="V1_0">
<Metadata>
<Atom/>
</Metadata>
<OperationName kb="ROR" kxe="false">AddManagedSystem</OperationName>
<GroupName kb="ROR" kxe="false">ManagementConsole</GroupName>
</RequestedOperation>
<JobParameters kb="CUR" kxe="false" schemaVersion="V1_0">
<Metadata>
<Atom/>
</Metadata>
<JobParameter schemaVersion="V1_0"><Metadata><Atom/></Metadata>
<ParameterName kb="ROR" kxe="false">host</ParameterName>
<ParameterValue kb="CUR" kxe="false">{{.Ipaddress}}</ParameterValue>
</JobParameter>
<JobParameter schemaVersion="V1_0"><Metadata><Atom/></Metadata>
<ParameterName kb="ROR" kxe="false">password</ParameterName>
<ParameterValue kb="CUR" kxe="false">abcc123</ParameterValue>
</JobParameter>
</JobParameters>
</JobRequest:JobRequest> `
tmpl := template.New("addManagedTemplate")
tmpl.Parse(addManagedTemplate)
addrequest := new(bytes.Buffer)
err := tmpl.Execute(addrequest, i)
if err != nil {
log.Fatal(err)
}
request, err := http.NewRequest("PUT", addManagedURl, addrequest)
// set request headers
request.Header.Set("Content-Type", "application/vnd.ibm.powervm.web+xml; type=JobRequest")
request.Header.Set("Accept", "application/atom+xml; charset=UTF-8")
// request.Header.Set("Expect", "")
response, err := s.client.Do(request)
fmt.Println("\n")
fmt.Println(response)
if err != nil {
log.Fatal(err)
} else {
defer response.Body.Close()
if response.StatusCode != 200 {
log.Fatalf("Error status code: %d", response.StatusCode)
}
}
}
func readPassword(prompt string) string {
fmt.Print(prompt)
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
println()
return string(pass)
}
func main() {
// Variables
password := readPassword("hmc Password: ")
//confirm := readPassword("Confirm hmc Password: ")
user := flag.String("user", "", "hmc user")
password := flag.String("password", "", "hmc user password")
url := flag.String("url", "", "hmc REST api url")
ipAddress := flag.String("ipAddress", "", "ip address of the managed server to be added")
flag.Parse()
//initialize new http session
fmt.Printf("server %s is being added.../n", *ipAddress)
session := NewSession(*user, *password, *url)
//var password string = readPassword("hmc password")
session.doLogon()
hmcUUID := session.getManaged()
ipaddr := &Manage {
Ipaddress: *ipAddress,
}
session.addManaged(&hmcUUID, ipaddr)
fmt.Println(hmcUUID)
}
Have a look at golang.org/x/crypto/ssh/terminal.
You can do something like this:
import (
"bufio"
"bytes"
"fmt"
"os"
"sync"
"syscall"
"golang.org/x/crypto/ssh/terminal"
)
func ReadPass() (password *bytes.Buffer, err error) {
var fi, _ = os.Stdin.Stat()
var pass []byte
isChardev := fi.Mode()&os.ModeCharDevice != 0
isNamedPipe := fi.Mode()&os.ModeNamedPipe != 0
if ! isChardev || isNamedPipe {
sin := bufio.NewReader(os.Stdin)
pass, _, err = sin.ReadLine()
} else {
_, _ = os.Stderr.WriteString(fmt.Sprint("passphrase: "))
pass, err = terminal.ReadPassword(syscall.Stdin)
if err != nil {
return nil, err
}
defer fmt.Println()
}
return bytes.NewBuffer(pass), err
}
The above example is taken from here:
https://github.com/tox2ik/go-smux/blob/master/io/terminal.go

Unit Test: SMTP mock server, for testing SMTP send email

I have test email logic, to send email, that is working fine.
Now to unit test that, I copied mock SMTP server code from https://golang.org/src/net/smtp/smtp_test.go, line 639 function => TestSendMailWithAuth
Mail code that works :
Types used
type Email struct {
Subject string
Body string
TemplateFile string
TemplateData interface{}
}
type pluginInput struct{
SMTPServer string
Username string
Password string
From string
To string
Cc string
}
email := &Email{
Subject: "SendMail test",
Body: `<!DOCTYPE html>
<html lang="en">
<head>
<title>IDP Execution Summary Email</title>
</head>
<body>test email</body>
</html>`,
}
Code that sends email
func (email *Email) SendMail(pluginInput *PluginInput) error {
// Connect to the remote SMTP server.
smtpClient, err := smtp.Dial(pluginInput.SMTPServer)
if err != nil {
logger.Error(err)
return err
}
//smtpServerHost
smtpServerHost, _, err := net.SplitHostPort(pluginInput.SMTPServer)
//start tls with no certificate check
if ok, _ := smtpClient.Extension("STARTTLS"); ok {
// #nosec G402
config := &tls.Config{ServerName: smtpServerHost, InsecureSkipVerify: true}
if err = smtpClient.StartTLS(config); err != nil {
return err
}
}
//set smtp client auth
if ok, authMechanism := smtpClient.Extension("AUTH"); ok {
userNameWithoutDomain := strings.Split(pluginInput.Username, "#")[0]
switch authMechanism {
case ``:
err = smtpClient.Auth(nil)
case `LOGIN`:
err = smtpClient.Auth(LoginAuth(userNameWithoutDomain, pluginInput.Password))
case `CRAM-MD5`:
err = smtpClient.Auth(smtp.CRAMMD5Auth(userNameWithoutDomain, pluginInput.Password))
case `PLAIN`:
err = smtpClient.Auth(smtp.PlainAuth("", userNameWithoutDomain, pluginInput.Password, smtpServerHost))
default:
err = smtpClient.Auth(smtp.PlainAuth("", userNameWithoutDomain, pluginInput.Password, smtpServerHost))
}
if err != nil {
return err
}
}
// From
if err = smtpClient.Mail(pluginInput.Username); err != nil {
logger.Error(err)
return err
}
// To & Cc
toArr := strings.Split(pluginInput.To, ",")
ccArr := strings.Split(pluginInput.Cc, ",")
toArr = append(toArr, ccArr...)
for _, addr := range toArr {
if err = smtpClient.Rcpt(addr); err != nil {
return err
}
}
//body
msg := "To: " + pluginInput.To + "\r\n" +
"Cc: " + pluginInput.Cc + "\r\n" +
"Subject: " + email.Subject + "\r\n" +
mIMEHeaders + "\r\n" + email.Body
// send Data command tp smtp server
smtpWriterCloser, err := smtpClient.Data()
if err != nil {
return err
}
_, err = fmt.Fprintf(smtpWriterCloser, msg)
if err != nil {
return err
}
if err = smtpWriterCloser.Close(); err != nil {
return err
}
//send Quit command to SMTP server
if err = smtpClient.Quit(); err != nil {
return err
}
return nil
}
Unit test code for this:
func TestEmail_SendMail(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to create listener: %v", err)
}
defer l.Close()
errCh := make(chan error)
go func() {
defer close(errCh)
conn, err := l.Accept()
if err != nil {
errCh <- fmt.Errorf("Accept: %v", err)
return
}
defer conn.Close()
tc := textproto.NewConn(conn)
tc.PrintfLine("220 hello world")
msg, err := tc.ReadLine()
if err != nil {
errCh <- fmt.Errorf("ReadLine error: %v", err)
return
}
const wantMsg = "EHLO localhost"
if msg != wantMsg {
errCh <- fmt.Errorf("unexpected response %q; want %q", msg, wantMsg)
return
}
err = tc.PrintfLine("250 mx.google.com at your service")
if err != nil {
errCh <- fmt.Errorf("PrintfLine: %v", err)
return
}
}()
type fields struct {
Subject string
Body string
TemplateFile string
TemplateData interface{}
}
type args struct {
pluginInput *PluginInput
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "without tls email send testing",
fields: fields{
Subject: "SendMail test",
Body: `<!DOCTYPE html>
<html lang="en">
<head>
<title>IDP Execution Summary Email</title>
</head>
<body>test email</body>
</html>`,
},
args: args{&PluginInput{
SMTPServer: l.Addr().String(),
Username: "test#example.com",
To: "other#example.com",
Cc: "another#example.com",
}},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
email := &Email{
Subject: tt.fields.Subject,
Body: tt.fields.Body,
TemplateFile: tt.fields.TemplateFile,
TemplateData: tt.fields.TemplateData,
}
if err := email.SendMail(tt.args.pluginInput); (err != nil) != tt.wantErr {
log.Print(err)
t.Errorf("Email.SendMail() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
err = <-errCh
if err != nil {
t.Fatalf("server error: %v", err)
}
}
After running unit test it is giving "EOF" error.
I'm not very sure about mock server, because it is not having switches where based on client request it sends response.
It happened because your implementation doesn't know, when it should close active connection.
Take a look on smtpmock package: https://github.com/mocktools/go-smtp-mock. This mock server is already designed for your case or even more :)

Golang multipart file form request

I'm writing an API client against Mapbox, uploading a batch of svg images to a custom map. The api they provide for this is documented with an example cUrl call that works fine:
curl -F images=#include/mapbox/sprites_dark/aubergine_selected.svg "https://api.mapbox.com/styles/v1/<my_company>/<my_style_id>/sprite?access_token=$MAPBOX_API_KEY" --trace-ascii /dev/stdout
When attemting to do the same from golang I quickly came across that the multiform library is very limited, and wrote some code to make the request as similar to the cUrl request mentioned above.
func createMultipartFormData(fileMap map[string]string) (bytes.Buffer, *multipart.Writer) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
var fw io.Writer
for fileName, filePath := range fileMap {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "images", fileName))
h.Set("Content-Type", "image/svg+xml")
if fw, err = w.CreatePart(h); err != nil {
fmt.Printf("Error creating form File %v, %v", fileName, err)
continue
}
fileContents, err := ioutil.ReadFile(filePath)
fileContents = bytes.ReplaceAll(fileContents, []byte("\n"), []byte("."))
blockSize := 64
remainder := len(fileContents) % blockSize
iterations := (len(fileContents) - remainder) / blockSize
newBytes := []byte{}
for i := 0; i < iterations; i++ {
start := i * blockSize
end := i*blockSize + blockSize
newBytes = append(newBytes, fileContents[start:end]...)
newBytes = append(newBytes, []byte("\n")...)
}
if remainder > 0 {
newBytes = append(newBytes, fileContents[iterations*blockSize:]...)
newBytes = append(newBytes, []byte("\n")...)
}
if err != nil {
fmt.Printf("Error reading svg file: %v: %v", filePath, err)
continue
}
_, err = fw.Write(newBytes)
if err != nil {
log.Debugf("Could not write file to multipart: %v, %v", fileName, err)
continue
}
}
w.Close()
return b, w
}
Along with setting the headers in the actual request:
bytes, formWriter := createMultipartFormData(filesMap)
req, err := http.NewRequest("Post", fmt.Sprintf("https://api.mapbox.com/styles/v1/%v/%v/sprite?access_token=%v", "my_company", styleID, os.Getenv("MAPBOX_API_KEY")), &bytes)
if err != nil {
return err
}
req.Header.Set("User-Agent", "curl/7.64.1")
req.Header.Set("Accept", "*/*")
req.Header.Set("Content-Length", fmt.Sprintf("%v", len(bytes.Bytes())))
req.Header.Set("Content-Type", formWriter.FormDataContentType())
byts, _ := httputil.DumpRequest(req, true)
fmt.Println(string(byts))
res, err := http.DefaultClient.Do(req)
Even want as far to limit the line length and replicate the encoding used by cUrl but so far no luck. Does anyone with experience know why this works from cUrl but not golang?
Well, I admit that all the parts of the "puzzle" to solve your task can be found on the 'net in abundance, there are two problems with this:
They quite often miss certain interesting details.
Sometimes, they give outright incorrect advice.
So, here's a working solution.
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
)
func main() {
const (
dst = "https://api.mapbox.com/styles/v1/AcmeInc/Style_001/sprite"
fname = "path/to/a/sprite/image.svg"
token = "an_invalid_token"
)
err := post(dst, fname, token)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func post(dst, fname, token string) error {
u, err := url.Parse(dst)
if err != nil {
return fmt.Errorf("failed to parse destination url: %w", err)
}
form, err := makeRequestBody(fname)
if err != nil {
return fmt.Errorf("failed to prepare request body: %w", err)
}
q := u.Query()
q.Set("access_token", token)
u.RawQuery = q.Encode()
hdr := make(http.Header)
hdr.Set("Content-Type", form.contentType)
req := http.Request{
Method: "POST",
URL: u,
Header: hdr,
Body: ioutil.NopCloser(form.body),
ContentLength: int64(form.contentLen),
}
resp, err := http.DefaultClient.Do(&req)
if err != nil {
return fmt.Errorf("failed to perform http request: %w", err)
}
defer resp.Body.Close()
_, _ = io.Copy(os.Stdout, resp.Body)
return nil
}
type form struct {
body *bytes.Buffer
contentType string
contentLen int
}
func makeRequestBody(fname string) (form, error) {
ct, err := getImageContentType(fname)
if err != nil {
return form{}, fmt.Errorf(
`failed to get content type for image file "%s": %w`,
fname, err)
}
fd, err := os.Open(fname)
if err != nil {
return form{}, fmt.Errorf("failed to open file to upload: %w", err)
}
defer fd.Close()
stat, err := fd.Stat()
if err != nil {
return form{}, fmt.Errorf("failed to query file info: %w", err)
}
hdr := make(textproto.MIMEHeader)
cd := mime.FormatMediaType("form-data", map[string]string{
"name": "images",
"filename": fname,
})
hdr.Set("Content-Disposition", cd)
hdr.Set("Contnt-Type", ct)
hdr.Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
part, err := mw.CreatePart(hdr)
if err != nil {
return form{}, fmt.Errorf("failed to create new form part: %w", err)
}
n, err := io.Copy(part, fd)
if err != nil {
return form{}, fmt.Errorf("failed to write form part: %w", err)
}
if int64(n) != stat.Size() {
return form{}, fmt.Errorf("file size changed while writing: %s", fd.Name())
}
err = mw.Close()
if err != nil {
return form{}, fmt.Errorf("failed to prepare form: %w", err)
}
return form{
body: &buf,
contentType: mw.FormDataContentType(),
contentLen: buf.Len(),
}, nil
}
var imageContentTypes = map[string]string{
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"svg": "image/svg+xml",
}
func getImageContentType(fname string) (string, error) {
ext := filepath.Ext(fname)
if ext == "" {
return "", fmt.Errorf("file name has no extension: %s", fname)
}
ext = strings.ToLower(ext[1:])
ct, found := imageContentTypes[ext]
if !found {
return "", fmt.Errorf("unknown file name extension: %s", ext)
}
return ct, nil
}
Some random notes on implementation to help you understands the concepts:
To construct the request's payload (body), we use a bytes.Buffer instance.
It has a nice property in that a pointer to it (*bytes.Buffer) implements both io.Writer and io.Reader and hence can be easily composed with other parts of the Go stdlib which deal with I/O.
When preparing a multipart form for sending, we do not slurp the whole file's contents into memory but instead "pipe" them right into the "mutipart form writer".
We have a lookup table which maps the extension of a file name to submit to its MIME type; I have no idea whether this is needed by the API or not; if it's not really required, the part of the code which prepares a form's field containing a file could be simplified a lot, but cURL send it, so do we.
Just to be curious, What is this for?
fileContents = bytes.ReplaceAll(fileContents, []byte("\n"), []byte("."))
blockSize := 64
remainder := len(fileContents) % blockSize
iterations := (len(fileContents) - remainder) / blockSize
newBytes := []byte{}
for i := 0; i < iterations; i++ {
start := i * blockSize
end := i*blockSize + blockSize
newBytes = append(newBytes, fileContents[start:end]...)
newBytes = append(newBytes, []byte("\n")...)
}
if remainder > 0 {
newBytes = append(newBytes, fileContents[iterations*blockSize:]...)
newBytes = append(newBytes, []byte("\n")...)
}
if err != nil {
fmt.Printf("Error reading svg file: %v: %v", filePath, err)
continue
}
Reading a entire file into memory is rarely a good idea (ioutil.ReadFile).
As #muffin-top says, how about those three lines of code?
for fileName, filePath := range fileMap {
// h := ...
fw, _ := w.CreatePart(h) // TODO: handle error
f, _ := os.Open(filePath) // TODO: handle error
io.Copy(fw, f) // TODO: handle error
f.Close() // TODO: handle error
}

Want to add a FormFile in unit test Golang

I want to test a httpRequest with a json body and a test file.
I don't know how to add the created test file to the request beside body json.
body := strings.NewReader(URLTest.RequestBody)
request, err := http.NewRequest(URLTest.MethodType, "localhost:"+string(listeningPort)+URLTest.URL, body)
if err != nil {
t.Fatalf("HTTP NOT WORKING")
}
fileBuffer := new(bytes.Buffer)
mpWriter := multipart.NewWriter(fileBuffer)
fileWriter, err := mpWriter.CreateFormFile("file", "testfile.pdf")
if err != nil {
t.Fatalf(err.Error())
}
file, err := os.Open("testfile.pdf")
if err != nil {
t.Fatalf(err.Error())
}
defer file.Close()
_, err = io.Copy(fileWriter, file)
if err != nil {
t.Fatalf(err.Error())
}
rec := httptest.NewRecorder()
UploadFiles(rec, request, nil)
response := rec.Result()
if response.StatusCode != URLTest.ExpectedStatusCode {
t.Errorf(URLTest.URL + " status mismatch")
}
responseBody, err := ioutil.ReadAll(response.Body)
defer response.Body.Close()
if err != nil {
t.Errorf(URLTest.URL + " cant read response")
} else {
if strings.TrimSpace(string(responseBody)) != URLTest.ExpectedResponseBody {
t.Errorf(URLTest.URL + " response mismatch - have: " + string(responseBody) + " want: " + URLTest.ExpectedResponseBody)
}
}
}
Can I add file as a value like request.FormFile.Add(...) or something?
Regarding your question about how to send a file in an HTTP request with Go, here's some sample code.
And you will need the mime/multipart package to build the form.
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/http/httputil"
"os"
"strings"
)
func main() {
var client *http.Client
var remoteURL string
{
//setup a mocked http client.
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := httputil.DumpRequest(r, true)
if err != nil {
panic(err)
}
fmt.Printf("%s", b)
}))
defer ts.Close()
client = ts.Client()
remoteURL = ts.URL
}
//prepare the reader instances to encode
values := map[string]io.Reader{
"file": mustOpen("main.go"), // lets assume its this file
"other": strings.NewReader("hello world!"),
}
err := Upload(client, remoteURL, values)
if err != nil {
panic(err)
}
}
func Upload(client *http.Client, url string, values map[string]io.Reader) (err error) {
// Prepare a form that you will submit to that URL.
var b bytes.Buffer
w := multipart.NewWriter(&b)
for key, r := range values {
var fw io.Writer
if x, ok := r.(io.Closer); ok {
defer x.Close()
}
// Add an image file
if x, ok := r.(*os.File); ok {
if fw, err = w.CreateFormFile(key, x.Name()); err != nil {
return
}
} else {
// Add other fields
if fw, err = w.CreateFormField(key); err != nil {
return
}
}
if _, err = io.Copy(fw, r); err != nil {
return err
}
}
// Don't forget to close the multipart writer.
// If you don't close it, your request will be missing the terminating boundary.
w.Close()
// Now that you have a form, you can submit it to your handler.
req, err := http.NewRequest("POST", url, &b)
if err != nil {
return
}
// Don't forget to set the content type, this will contain the boundary.
req.Header.Set("Content-Type", w.FormDataContentType())
// Submit the request
res, err := client.Do(req)
if err != nil {
return
}
// Check the response
if res.StatusCode != http.StatusOK {
err = fmt.Errorf("bad status: %s", res.Status)
}
return
}
Hope you can use this in your unit test

Send email with attachments in golang

Here is the code:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/user"
"path/filepath"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/gmail/v1"
"encoding/base64"
"io/ioutil"
)
// 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, configFileName string) *http.Client {
cacheFile, err := tokenCacheFile(configFileName)
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(filename string) (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(filename)), 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()
json.NewEncoder(f).Encode(token)
}
func main() {
// Use oauth2.NoContext if there isn't a good context to pass in.
//ctx := context.TODO()
ctx := context.Background()
b, err := ioutil.ReadFile("client_secret.json")
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/gmail-go-quickstart.json
sendConfig, err := google.ConfigFromJSON(b, gmail.GmailSendScope)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
sendClient := getClient(ctx, sendConfig, "send.json")
sendService, err := gmail.New(sendClient)
if err != nil {
log.Fatalf("Unable to retrieve gmail Client %v", err)
}
if err := SendEmail(ctx, sendService, "jane1988#gmail.com"); err != nil {
log.Fatalf("failed to send email: %v", err)
}
}
func SendEmail(ctx context.Context, svc *gmail.Service, email string) error {
header := make(map[string]string)
header["To"] = email
header["Subject"] = "hello there"
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\n", k, v)
}
msg += "\n" + "Hello, Gmail!"
gmsg := gmail.Message{
Raw: encodeWeb64String([]byte(msg)),
}
_, err := svc.Users.Messages.Send("me", &gmsg).Do()
return err
}
func encodeWeb64String(b []byte) string {
s := base64.URLEncoding.EncodeToString(b)
var i = len(s) - 1
for s[i] == '=' {
i--
}
return s[0 : i + 1]
}
This works perfectly, but without attachments. How can I attach files to the mail?
Maybe you can try change the header Content-Type to multipart/mixed (RFC 2046, Section 5.1.3) or multipart/alternative (RFC 2046, Section 5.1.4) and check how to use Content-Disposition: attachment; filename=<your file here.ext>.

Resources