Setting up standard Go net/smtp with Office 365 fails with "Error tls: first record does not look like a TLS handshake"

I'm trying to create a simple Go emailing service using the default Go packages net/smtp - I know there's gomailer, but i'd like to use the standard library

I need help with configuring the tls/server setting to work with Office365

I believe that I have the correct host:

smtp.office365.com:587

From copying the documentation for smtp that Microsoft provide, however, I get the following error in my console when running the below code:

Error: tls: first record does not look like a TLS handshake panic: runtime error: invalid memory address or nil pointer dereference

package main

import (
"fmt"
"net"
mail "net/mail"
smtp "net/smtp"
)

func main() {

from := mail.Address{"", "[email protected]"}
to := mail.Address{"", "[email protected]"}
subject := "My test subject"
body := "Test email body"

// Setup email headers
headers := make(map[string]string)
headers["From"] = from.String()
headers["To"] = to.String()
headers["Subject"] = subject

message := ""
for k, v := range headers {
    message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += "\r\n" + body

servername := "smtp.office365.com:587"
host, _, _ := net.SplitHostPort(servername)

auth := smtp.PlainAuth("", "[email protected]", "password", host)

tlsconfig := &tls.Config{
    InsecureSkipVerify: true,
    ServerName:         host,
}

conn, err := tls.Dial("tcp", "smtp.office365.com:587", tlsconfig)
if err != nil {
    fmt.Println("tls.Dial Error: %s", err)
}

c, err := smtp.NewClient(conn, host)
if err != nil {
    fmt.Println("smtp.NewClient Error: %s", err)
}

if err = c.Auth(auth); err != nil {
    fmt.Println("c.Auth Error: %s", err)
}

if err = c.Mail(from.Address); err != nil {
    fmt.Println("c.Mail Error: %s", err)
}

if err = c.Rcpt(to.Address); err != nil {
    fmt.Println("c.Rcpt Error: %s", err)
}

w, err := c.Data()
if err != nil {
    fmt.Println("c.Data Error: %s", err)
}

_, err = w.Write([]byte(message))
if err != nil {
    fmt.Println("Error: %s", err)
}

err = w.Close()
if err != nil {
    fmt.Println("reader Error: %s", err)
}

c.Quit()
}

Any examples of an O365 client will be appreciated, or anything that anyone can spot that seems suspect will be great

Thanks

Upvotes: 3

Views: 5113

Answers (4)

Hasan A Yousef
Hasan A Yousef

Reputation: 24988

Below worked fine with me:

package main

import (
    "bytes"
    "crypto/tls"
    "errors"
    "fmt"
    "net"
    "net/smtp"
    "text/template"
)

type loginAuth struct {
    username, password string
}

func LoginAuth(username, password string) smtp.Auth {
    return &loginAuth{username, password}
}

func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
    return "LOGIN", []byte(a.username), nil
}

func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
    if more {
        switch string(fromServer) {
        case "Username:":
            return []byte(a.username), nil
        case "Password:":
            return []byte(a.password), nil
        default:
            return nil, errors.New("Unknown from server")
        }
    }
    return nil, nil
}

func main() {

    // Sender data.
    from := "O365 logging name"
    password := "O365 logging pasword"

    // Receiver email address.
    to := []string{
        "receiver email",
    }

    // smtp server configuration.
    smtpHost := "smtp.office365.com"
    smtpPort := "587"

    conn, err := net.Dial("tcp", "smtp.office365.com:587")
    if err != nil {
        println(err)
    }

    c, err := smtp.NewClient(conn, smtpHost)
    if err != nil {
        println(err)
    }

    tlsconfig := &tls.Config{
        ServerName: smtpHost,
    }

    if err = c.StartTLS(tlsconfig); err != nil {
        println(err)
    }

    auth := LoginAuth(from, password)

    if err = c.Auth(auth); err != nil {
        println(err)
    }

    t, _ := template.ParseFiles("template.html")

    var body bytes.Buffer

    mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
    body.Write([]byte(fmt.Sprintf("Subject: This is a test subject \n%s\n\n", mimeHeaders)))

    t.Execute(&body, struct {
        Name    string
        Message string
    }{
        Name:    "Hasan Yousef",
        Message: "This is a test message in a HTML template",
    })

    // Sending email.
    err = smtp.SendMail(smtpHost+":"+smtpPort, auth, from, to, body.Bytes())
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("Email Sent!")
}

With the below template as bonus :)

<!-- template.html -->
<!DOCTYPE html>
<html>
<body>
    <h3>Name:</h3><span>{{.Name}}</span><br/><br/>
    <h3>Email:</h3><span>{{.Message}}</span><br/>
</body>
</html>

Upvotes: 1

Joe Chen
Joe Chen

Reputation: 41

  1. Outlook.com no longer supports AUTH PLAIN authentication since August 2017.

https://support.microsoft.com/en-us/office/outlook-com-no-longer-supports-auth-plain-authentication-07f7d5e9-1697-465f-84d2-4513d4ff0145?ui=en-us&rs=en-us&ad=us

  1. Use AUTH LOGIN

The following codes implement AUTH LOGIN

type loginAuth struct {
    username, password string
}

func LoginAuth(username, password string) smtp.Auth {
    return &loginAuth{username, password}
}


func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
    return "LOGIN", []byte(a.username), nil
}


func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
    if more {
        switch string(fromServer) {
        case "Username:":
            return []byte(a.username), nil
        case "Password:":
            return []byte(a.password), nil
        default:
            return nil, errors.New("Unknown from server")
        }
    }
    return nil, nil
}
  1. remove "InsecureSkipVerify: true"
tlsconfig := &tls.Config {
    ServerName: host,
}
  1. don't use tsl.Dial(), use net.Dial()
conn, err := net.Dial("tcp", "smtp.office365.com:587")
if err != nil {
    return err
}
  1. call StartTLS() after smtp.NewClient()
c, err := smtp.NewClient(conn, host)
if err != nil {
    return err
}

if err = c.StartTLS(tlsconfig); err != nil {
    return err
}
  1. use AUTH LOGIN
auth := LoginAuth(fromAddress, password) 

if err = c.Auth(auth); err != nil {
    return err
}

Upvotes: 4

So the issue was all about authorisation. Firstly requiring that I use the StartTLS method on the client, and also that I write a function and methods to support LOGIN, something that the standard Go library doesn't support (for whatever reason)

See the functions and struct above the main()

Here's the full code, with the helper function, that can now successfully send an email through my O365 account:

package main

import (
"fmt"
"net"
"errors"
mail "net/mail"
smtp "net/smtp"
)

type loginAuth struct {
    username, password string
}

func LoginAuth(username, password string) smtp.Auth {
    return &loginAuth{username, password}
}

func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
    return "LOGIN", []byte{}, nil
}

func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
    if more {
        switch string(fromServer) {
        case "Username:":
            return []byte(a.username), nil
        case "Password:":
            return []byte(a.password), nil
        default:
            return nil, errors.New("Unknown fromServer")
        }
    }
    return nil, nil
}

func main() {

from := mail.Address{"", "[email protected]"}
to := mail.Address{"", "[email protected]"}
subject := "My test subject"
body := "Test email body"

headers := make(map[string]string)
headers["From"] = from.String()
headers["To"] = to.String()
headers["Subject"] = subject

message := ""
for k, v := range headers {
    message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += "\r\n" + body

tlsconfig := &tls.Config{
    ServerName:         host,
}

conn, err := tls.Dial("tcp", "smtp.office365.com:587", tlsconfig)
if err != nil {
    fmt.Println("tls.Dial Error: ", err)
}

c, err := smtp.NewClient(conn, host)
if err != nil {
    fmt.Println("smtp.NewClient Error: ", err)
}


if err = c.Auth(LoginAuth("[email protected]", "password")); err != nil {
        fmt.Println("c.Auth Error: ", err)
        return
}

if err = c.Mail(from.Address); err != nil {
    fmt.Println("c.Mail Error: ", err)
}

if err = c.Rcpt(to.Address); err != nil {
    fmt.Println("c.Rcpt Error: ", err)
}

w, err := c.Data()
if err != nil {
    fmt.Println("c.Data Error: ", err)
}

_, err = w.Write([]byte(message))
if err != nil {
    fmt.Println("Error: ", err)
}

err = w.Close()
if err != nil {
    fmt.Println("reader Error: ", err)
}

c.Quit()
}

Upvotes: 1

marco.m
marco.m

Reputation: 4859

The error message Error: tls: first record does not look like a TLS handshake is telling you what the problem is :-). If you try connecting to the server, you will see that (as any SMTP servers) it uses plain text:

telnet smtp.office365.com 587
Trying 2603:1026:c0b:10::2...
Connected to zrh-efz.ms-acdc.office.com.
Escape character is '^]'.
220 ZRAP278CA0003.outlook.office365.com Microsoft ESMTP MAIL Service ready at Mon, 11 Nov 2019 17:13:50 +0000
...

You need to use the STARTTLS command, see https://en.wikipedia.org/wiki/Opportunistic_TLS (and the RFCs pointed by that wiki page).

In Go, it is https://golang.org/pkg/net/smtp/#Client.StartTLS.

In your code I noticed

tlsconfig := &tls.Config{
    InsecureSkipVerify: true,   <== REMOVE THIS
    ServerName:         host,
}

Please remove the InsecureSkipVerify, it is, as the name implies, insecure and has nothing to do with the error you are facing.

Upvotes: 3

Related Questions