Unable to connect to psql db - postgresql

I am trying to connect to a database, but getting an error when I make a curl request to the endpoint with the GET method. I double-checked with the user credentials and have granted full privileges and superuser permissions.
Following is the error I get when curl the endpoint:
santosh#pkg*$:curl -i localhost:8080/books/show
HTTP/1.1 303 See Other
Content-Type: text/html; charset=utf-8
Location: /books
Date: Sat, 19 Nov 2022 12:09:52 GMT
Content-Length: 33
See Other.
The connection is established with the Database, when the request is made to the database these errors are triggered:
santosh#pkg*$:go run main.go
Database connection successful.
2022/11/19 17:39:47 http: panic serving 127.0.0.1:44324: runtime error: invalid memory address or nil pointer dereference
goroutine 35 [running]:
net/http.(*conn).serve.func1()
/usr/local/go/src/net/http/server.go:1850 +0xbf
panic({0x6960e0, 0x8e5630})
/usr/local/go/src/runtime/panic.go:890 +0x262
database/sql.(*DB).conn(0x0, {0x7593d0, 0xc00011a000}, 0x1)
/usr/local/go/src/database/sql/sql.go:1288 +0x53
database/sql.(*DB).query(0x6?, {0x7593d0, 0xc00011a000}, {0x6da967, 0x13}, {0x0, 0x0, 0x0}, 0x68?)
The main program:
var db *sql.DB
type Books struct {
Isbn string
Title string
Author string
Price float32
}
func init() {
var err error
args := fmt.Sprintf("host=%s port=%d dbname=%s user='%s' password=%s sslmode=%s", "localhost", 5432, "bookstore", "santosh", "dts123", "disable")
db, err := sql.Open("postgres", args)
if err != nil {
fmt.Printf("Creating Database %s", err)
}
if err = db.Ping(); err != nil {
panic(err)
}
fmt.Println("Database connection succussful.")
}
func main() {
http.HandleFunc("/", index)
http.HandleFunc("/books", booksIndex)
http.ListenAndServe(":8080", nil)
}
func index(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/books", http.StatusSeeOther)
}
func booksIndex(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), http.StatusMethodNotAllowed)
return
}
rows, err := db.Query("SELECT * FROM books")
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
defer rows.Close()
bks := make([]Books, 0)
for rows.Next() {
bk := Books{}
err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price)
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
bks = append(bks, bk)
}
if err = rows.Err(); err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
}
I tried double-checking user privileges and database format and for order. All are in line with the code. The connection is established but fails with panic while querying the DB.

You are not properly initializing the package-level db variable.
The := operator, called "short variable declaration", declares and initializes a new variable in its block scope. Any variable with the same name in an outer scope will be "shadowed".
To properly initialize the package-level variable you can use plain assignment:
var db *sql.DB
func init() {
args := fmt.Sprintf("host=%s port=%d dbname=%s user='%s' password=%s sslmode=%s", "localhost", 5432, "bookstore", "santosh", "dts123", "disable")
var err error
db, err = sql.Open("postgres", args)
if err != nil {
fmt.Printf("Creating Database %s", err)
}
// ...
}
Or you can use := but then use a different variable name and make sure to use that for the assignment:
var db *sql.DB
func init() {
args := fmt.Sprintf("host=%s port=%d dbname=%s user='%s' password=%s sslmode=%s", "localhost", 5432, "bookstore", "santosh", "dts123", "disable")
_db, err := sql.Open("postgres", args)
if err != nil {
fmt.Printf("Creating Database %s", err)
}
// ...
db = _db // set "global"
}

Related

Mongo-Driver Should I use InsertMany or InsertOne

I am creating a Rest CRUD HTTP server written in Go. My error is I am getting this message from the database connection: "context deadline exceeded".
I have a CreateUsers() function to insert multiple users into the database. Currently, I am inserting one user at time:
func CreateUsers(users []*models.User) ([]primitive.ObjectID, error) {
client, ctx, cancel := database.GetConnection()
defer cancel()
defer client.Disconnect(ctx)
var userIds []primitive.ObjectID
if len(users) == 0 {
log.Printf("No users to create")
return userIds, errors.New("no users to create")
}
for _, user := range users {
user.ID = primitive.NewObjectID()
hashedPassword, err := utils.HashPassword(user.Password)
if err != nil {
log.Printf("Error while hashing password: %v", err)
return userIds, err
}
user.Password = hashedPassword
result, err := client.Database("users").Collection("users").InsertOne(ctx, user)
if err != nil {
log.Printf("Error while creating user: %v", err)
return userIds, err
}
oid := result.InsertedID.(primitive.ObjectID)
userIds = append(userIds, oid)
}
return userIds, nil
}
My database connection (database.GetConnection) is something like:
func GetConnection() (*mongo.Client, context.Context, context.CancelFunc) {
client, err := mongo.NewClient(options.Client().ApplyURI(connectionURI))
if err != nil {
log.Fatalf("error while creating client: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), connectionTimeout*time.Second)
err = client.Connect(ctx)
if err != nil {
log.Fatalf("cluster connection error: %v", err)
}
err = client.Ping(ctx, nil)
if err != nil {
log.Fatalf("cluster ping error")
}
log.Println("connected to mongodb")
return client, ctx, cancel
}
Note: connectionTimeout is equal to 5.
I am not sure but the time exceeded error may be related to the InsertOne() approach. So, instead of focusing in solving that error I should be asking how to parse the []models.User into bson.D{} documents to pass them as parameters to InsertMany().
I think another advantage of using InsertMany approach is the query time will be way below. Any suggestions?

Can I have insecure GET HTTP requests whilst having MTLS securing all other HTTP requests?

I have a HTTP REST service written in golang demonstrating what I'm attempting.
I want GET requests insecure and all other REST requests secured with MTLS.
My implementation already uses the gin web server library so I'd like to stick with that if possible.
My issue is that I have only been able to apply the tlsConfig to both groups or neither. I've been unable to find a way to apply this at the group level.
package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net/http"
"log"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Unprotected public router for GET requests
public := router.Group("/")
// Private router with MTLS
private := router.Group("/")
public.GET("/insecure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "insecure pong",
})
})
private.POST("/secure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "secure pong",
})
})
// Get the SystemCertPool, continue with an empty pool on error
rootCAs, err := x509.SystemCertPool()
if err != nil {
log.Fatal(err)
}
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
// Create a CA certificate pool and add cacert.pem to it
caCert, err := ioutil.ReadFile("cacert.pem")
if err != nil {
log.Fatal(err)
}
if ok := rootCAs.AppendCertsFromPEM(caCert); !ok {
err := errors.New("failed to append CA cert to local system certificate pool")
log.Fatal(err)
}
server := http.Server{
Addr: fmt.Sprintf(":%v", 8080),
Handler: router,
}
server.TLSConfig = &tls.Config{
RootCAs: rootCAs,
}
err = server.ListenAndServeTLS("certificate.crt", "privateKey.key")
if err != nil {
log.Fatal(err)
}
}
Just create two Server instances and run them both, one with ListenAndServe and one with ListenAndServeTLS, configured with the same routes. Because HTTP and HTTPS operate on different ports, they have to have different listeners, but both listeners can use the same (or different) handlers. For example:
publicRouter := gin.Default()
// Unprotected public router for GET requests
public := publicRouter.Group("/")
public.GET("/insecure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "insecure pong",
})
})
server := http.Server{
Addr: fmt.Sprintf(":%v", 8081), // Or whatever
Handler: publicRouter,
}
go func() {
err = tlsServer.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}()
// Private router with MTLS
router := gin.Default()
private := router.Group("/")
private.POST("/secure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "secure pong",
})
})
// Get the SystemCertPool, continue with an empty pool on error
rootCAs, err := x509.SystemCertPool()
if err != nil {
log.Fatal(err)
}
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
// Create a CA certificate pool and add cacert.pem to it
caCert, err := ioutil.ReadFile("cacert.pem")
if err != nil {
log.Fatal(err)
}
if ok := rootCAs.AppendCertsFromPEM(caCert); !ok {
err := errors.New("failed to append CA cert to local system certificate pool")
log.Fatal(err)
}
tlsServer := http.Server{
Addr: fmt.Sprintf(":%v", 8080),
Handler: router,
}
tlsServer.TLSConfig = &tls.Config{
RootCAs: rootCAs,
}
err = tlsServer.ListenAndServeTLS("certificate.crt", "privateKey.key")
if err != nil {
log.Fatal(err)
}

Connection(localhost:27017[-4]) failed to write: context canceled

Here I read it's necessary to cancel the context. This is how my db.go looks like:
package db
import (
"context"
"fmt"
"log"
"os"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
connectionStringTemplate = "mongodb://%s:%s#%s"
)
var DB *mongo.Client
var Ctx context.Context
// Connect with create the connection to MongoDB
func Connect() {
username := os.Getenv("MONGODB_USERNAME")
password := os.Getenv("MONGODB_PASSWORD")
clusterEndpoint := os.Getenv("MONGODB_ENDPOINT")
connectionURI := fmt.Sprintf(connectionStringTemplate, username, password, clusterEndpoint)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.NewClient(options.Client().ApplyURI(connectionURI))
if err != nil {
log.Printf("Failed to create client: %v", err)
}
err = client.Connect(ctx)
if err != nil {
log.Printf("Failed to connect to cluster: %v", err)
}
// Force a connection to verify our connection string
err = client.Ping(ctx, nil)
if err != nil {
log.Printf("Failed to ping cluster: %v", err)
}
DB = client
Ctx = ctx
log.Printf("Connected to MongoDB!")
}
I execute this in the main.go:
func main() {
// Configure
db.Connect()
defer db.DB.Disconnect(context.Background())
r := gin.Default()
// Routes
//r.GET("/movies", handlers.GetAllMoviesHandler)
r.POST("/movies", handlers.AddMovieHandler)
// listen and serve on 0.0.0.0:8080
r.Run()
}
and it works fine if my local mongodb is running.
Now when I use the client like this (I know the naming is still bad):
func AddMovie(movie *Movie) (primitive.ObjectID, error) {
movie.ID = primitive.NewObjectID()
result, err := db.DB.Database("movies").Collection("movies").InsertOne(db.Ctx, movie)
if err != nil {
log.Printf("Could not create movie: %v", err)
return primitive.NilObjectID, err
}
oid := result.InsertedID.(primitive.ObjectID)
return oid, nil
}
I get an error like this
{"msg":{"Code":0,"Message":"connection(localhost:27017[-4]) failed to write: context canceled","Labels":["NetworkError","RetryableWriteError"],"Name":"","Wrapped":{"ConnectionID":"localhost:27017[-4]","Wrapped":{}}}}
It works fine when I put the defer cancel() in comment but I suppose this is not correct?. What am I doing wrong here.
Update:
It works when I use:
result, err :=db.DB.Database("movies").Collection("movies").InsertOne(context.Background(), movie) instead of ctx. I don't fully understand the usage of the context in these use case. I'm not sure if I should do the Ctx = ctx in the Connect function either.

Why accepted two same 5-tuple socket when concurrent connect to the server?

server.go
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
_ "net/http/pprof"
"sync"
"syscall"
)
type ConnSet struct {
data map[int]net.Conn
mutex sync.Mutex
}
func (m *ConnSet) Update(id int, conn net.Conn) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, ok := m.data[id]; ok {
fmt.Printf("add: key %d existed \n", id)
return fmt.Errorf("add: key %d existed \n", id)
}
m.data[id] = conn
return nil
}
var connSet = &ConnSet{
data: make(map[int]net.Conn),
}
func main() {
setLimit()
ln, err := net.Listen("tcp", ":12345")
if err != nil {
panic(err)
}
go func() {
if err := http.ListenAndServe(":6060", nil); err != nil {
log.Fatalf("pprof failed: %v", err)
}
}()
var connections []net.Conn
defer func() {
for _, conn := range connections {
conn.Close()
}
}()
for {
conn, e := ln.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
log.Printf("accept temp err: %v", ne)
continue
}
log.Printf("accept err: %v", e)
return
}
port := conn.RemoteAddr().(*net.TCPAddr).Port
connSet.Update(port, conn)
go handleConn(conn)
connections = append(connections, conn)
if len(connections)%100 == 0 {
log.Printf("total number of connections: %v", len(connections))
}
}
}
func handleConn(conn net.Conn) {
io.Copy(ioutil.Discard, conn)
}
func setLimit() {
var rLimit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
panic(err)
}
rLimit.Cur = rLimit.Max
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
panic(err)
}
log.Printf("set cur limit: %d", rLimit.Cur)
}
client.go
package main
import (
"bytes"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"sync"
"syscall"
"time"
)
var portFlag = flag.Int("port", 12345, "port")
type ConnSet struct {
data map[int]net.Conn
mutex sync.Mutex
}
func (m *ConnSet) Update(id int, conn net.Conn) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, ok := m.data[id]; ok {
fmt.Printf("add: key %d existed \n", id)
return fmt.Errorf("add: key %d existed \n", id)
}
m.data[id] = conn
return nil
}
var connSet = &ConnSet{
data: make(map[int]net.Conn),
}
func echoClient() {
addr := fmt.Sprintf("127.0.0.1:%d", *portFlag)
dialer := net.Dialer{}
conn, err := dialer.Dial("tcp", addr)
if err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
port := conn.LocalAddr().(*net.TCPAddr).Port
connSet.Update(port, conn)
defer conn.Close()
for i := 0; i < 10; i++ {
s := fmt.Sprintf("%s", strconv.Itoa(i))
_, err := conn.Write([]byte(s))
if err != nil {
log.Println("write error: ", err)
}
b := make([]byte, 1024)
_, err = conn.Read(b)
switch err {
case nil:
if string(bytes.Trim(b, "\x00")) != s {
log.Printf("resp req not equal, req: %d, res: %s", i, string(bytes.Trim(b, "\x00")))
}
case io.EOF:
fmt.Println("eof")
break
default:
fmt.Println("ERROR", err)
break
}
}
time.Sleep(time.Hour)
if err := conn.Close(); err != nil {
log.Printf("client conn close err: %s", err)
}
}
func main() {
flag.Parse()
setLimit()
before := time.Now()
var wg sync.WaitGroup
for i := 0; i < 20000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
echoClient()
}()
}
wg.Wait()
fmt.Println(time.Now().Sub(before))
}
func setLimit() {
var rLimit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
panic(err)
}
rLimit.Cur = rLimit.Max
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
panic(err)
}
log.Printf("set cur limit: %d", rLimit.Cur)
}
running command
go run server.go
---
go run client.go
server running screenshot
The client simultaneously initiates 20,000 connections to the server, and the server accepted two remotePort connections that are exactly the same (in a extremely short period of time).
I try to use tcpconn.py from bcc (patched from tcpconnect.py by add skc_num(aka: local_port))
tcpaccept.py
tracing the connection, and also finds that the remote port is duplicated on the server side when there is no duplicate on the client side
In my understanding, the 5-tuple of the socket will not be duplicated, Why the server accepted two sockets with exactly the same remote port?
My test environment:
Fedora 31, kernel version 5.3.15 x86_64
and
Ubuntu 18.04.3 LTS, kernel version 4.19.1 x86_64
go version go1.13.5 linux/amd64
wireshark:
server TCP Keep-Alive to both ACK & PSH+ACK
server TCP Keep-Alive to PSH+ACK only
Connection is added to the map data map[int]net.Conn when it is established, but when connection is closed it is not removed from the map. So if connection gets closed its port become free and could be reused by an Operation System for next connection. That's a reason why you can see duplicate ports.
Try to remove port from map when they are get closed.

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{"", "example#example.com"}
to := mail.Address{"", "example#example.com"}
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("", "example#example.com", "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
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
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
}
remove "InsecureSkipVerify: true"
tlsconfig := &tls.Config {
ServerName: host,
}
don't use tsl.Dial(), use net.Dial()
conn, err := net.Dial("tcp", "smtp.office365.com:587")
if err != nil {
return err
}
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
}
use AUTH LOGIN
auth := LoginAuth(fromAddress, password)
if err = c.Auth(auth); err != nil {
return err
}
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.
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>
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{"", "example#example.com"}
to := mail.Address{"", "example#example.com"}
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("example#example.com", "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()
}