echo icon indicating copy to clipboard operation
echo copied to clipboard

How to parse string to mail.Address

Open apuatcfbd opened this issue 11 months ago • 5 comments

I am trying to parse the following

type NewEmail struct {
	From        mail.Address                 `form:"from"`
	// ...
}

with

// In handler
if err := c.Bind(dtoPtr); err != nil { // dtoPtr is *NewEmail
	errResponse = c.JSON(http.StatusBadRequest, Res(ResData{
		Msg: "Review your input",
		D:   err, // here's the error
	}))
	return
}

Getting following error unknown type

{
  "d": {
    "message": "unknown type"
  },
  "msg": "Review your input",
  "status": false
}

I've checked that it works if I make NewEmail structs From field to string from mail.Address. It is beneficial for me if I can parse to mail.Address based on my use case. How this can be done?

apuatcfbd avatar Feb 05 '25 11:02 apuatcfbd

Could you provide how mail.Address is declared? Is is struct or alias or ?

aldas avatar Feb 05 '25 11:02 aldas

The type mail.Address is from net/mail of StdLib That struct looks like the following

type Address struct {
    Name    string
    Address string
}

Echo version github.com/labstack/echo/v4 v4.13.3

apuatcfbd avatar Feb 06 '25 14:02 apuatcfbd

This

package main

import (
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"log/slog"
	"net/http"
	"net/mail"
)

func main() {
	e := echo.New()
	e.Use(middleware.Logger())

	e.POST("/", func(c echo.Context) error {
		type NewEmail struct {
			From mail.Address `form:"from"`
		}
		dto := &NewEmail{}
		if err := c.Bind(dto); err != nil { // dto is *NewEmail
			return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
		}
		return c.JSON(http.StatusOK, dto)
	})

	if err := e.Start(":8080"); err != nil {
		slog.Error("failed to start server")
	}
}

does not work because you can only bind forms to (struct) fields that are explicitly marked with struct tags. But currently the target is struct with additional fields - this is not supported.

You could create your own address type and add tags

	e.POST("/", func(c echo.Context) error {
		type Address struct {
			Name    string `form:"name"`
			Address string `form:"address"`
		}

		type NewEmail struct {
			From Address
		}
		dto := &NewEmail{}
		if err := c.Bind(dto); err != nil { // dto is *NewEmail
			return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
		}
		return c.JSON(http.StatusOK, dto)
	})

test with

curl --form name=testName --form address=testAddress http://localhost:8080/

aldas avatar Feb 06 '25 19:02 aldas

note: curl -H "Content-Type: application/json" --data '{"from":{"name":"test","address":"address"}}' http://localhost:8080/ would work with

	e.POST("/", func(c echo.Context) error {
		type NewEmail struct {
			From mail.Address
		}
		dto := &NewEmail{}
		if err := c.Bind(dto); err != nil { // dto is *NewEmail
			return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
		}
		return c.JSON(http.StatusOK, dto)
	})

but this is because standard library JSON unmarshalling supports this

aldas avatar Feb 06 '25 19:02 aldas

I know that this issue is old, but I'm writing this here just in case it could help someone else with a similar parsing issue, either for mail.Address or for any other struct.

I believe that what @apuatcfbd wants to do is parse the mail.Address from an RFC 5322 email address string, similar to the behavior of the mail.ParseAddress function. An RFC 5322 email address string is of the format:

John Doe <[email protected]>

which would then be parsed by mail.ParseAddress as:

mail.Address{Name: "John Doe", Address: "[email protected]"}

By default, Echo's binding functionality does not support the mail.Address struct. However, Echo bindings do support types that implement the interface encoding.TextUnmarshaler (see binder.go). So we can make our own email address struct that implements TextUnmarshaler as follows:

type mailAddressUnmarshalable struct {
	mail.Address
}

// Implement encoding.TextUnmarshaler
func (m *mailAddressUnmarshalable) UnmarshalText(text []byte) error {
	add, err := mail.ParseAddress(string(text))
	m.Address = *add
	return err
}

and update our NewEmail struct as follows:

type NewEmail struct {
	From mailAddressUnmarshalable `form:"from"`
}

Now the binding occurs properly, there is no error, and the struct field is filled with the correct value.

xtt28 avatar Aug 02 '25 18:08 xtt28