A custom domain email is quite valuable: it is memorable and professional, and it prevents cases where being banned from an email provider locks you out of services where you used that email address to sign up.

It took me quite some trial and error to get everything set up. So I’m writing a blog to share my setup.

This blog will address the following issues:

  1. How to use your custom domain email address to receive emails and get notified in China
  2. How to use your custom domain email address to send emails
  3. How to send emails from your custom domain email address programmatically in China

Receiving emails

Sure, you can get a dedicated mailbox, but for me, a better solution is to use services such as https://forwardmx.io or https://forwardemail.net. These services act as a forwarder: emails sent to your domain email address will get forwarded to one or more of your personal non-domain email addresses. (e.g., emails sent to me@ke.wang get forwarded to my ...@gmail.com and ...@qq.com).

The advantage of using services like these is that you can continue using the email providers that you are used to; there’s no need to switch.

I usually configure the email forwarding service so that the emails get forwarded to both my QQ Mail and my Gmail.

  • QQ Mail has very good support in China; email notifications (push notifications on mobile phones) are first-rate. I have one copy forwarded to my QQ Mail so that I can see notifications for new emails promptly.
  • Gmail is more reliable and professional. So I have my second copy forwarded to Gmail. Gmail is the only mainstream service that will allow sending emails from your custom domain email, so that’s another reason why I choose Gmail.

I will skip the set-up tutorial here, as you can find it in the knowledge base of email forwarding service providers. (It’s very easy, just set up MX records in your DNS, and set up rules in the email forwarding service provider. You should probably also set up TXT records and DKIM records; just follow the instructions from your forwarding service provider.)

Sending emails

See this article for instructions: https://forwardmx.net/billing/knowledgebase/5/Send-emails-with-Google-SMTP.html

Basically, you will need to configure Gmail to send emails through your custom SMTP server.

You can get the credentials for your custom SMTP server from your forwarding service provider.

Gmail is great because it will select the outbound email address automatically. That is to say, if you receive an email with your custom domain email address, when you reply to it in Gmail, the outbound email address is automatically set to your custom domain email address that you received the email with. You can also use a drop-down menu to choose the outbound email address manually.

So, basically, my workflow is that I get notified and see an email preview in QQ Mail, then I go to Gmail to reply to the emails.

Sending emails programmatically

I self-host services like Healthchecks.io, Sentry, and WordPress. Email notification is very important.

Sending emails programmatically should be as easy as configuring your program to use the SMTP server given by the forwarding service provider. However, what makes it very difficult is that the SMTP server from forwardmx.io is sometimes blocked in China.

I have to write a custom SMTP-forwarding proxy. In the forwarding process, it could use a reliable HTTP/HTTPS connect proxy (I set one up with bandwagonhost.com).

Here’s some throw-away code. I pasted the code together from

package main

import (
	"bufio"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"time"

	"github.com/emersion/go-sasl"
	"github.com/emersion/go-smtp"
	"golang.org/x/net/proxy"
)

const targetSMTPServer = "mx1.forwardmx.net:587"

func main() {
	be := &Backend{}

	s := smtp.NewServer(be)

	s.Addr = ":1025"
	s.Domain = "0.0.0.0"
	s.ReadTimeout = 10 * time.Second
	s.WriteTimeout = 10 * time.Second
	s.MaxMessageBytes = 1024 * 1024
	s.MaxRecipients = 50
	s.AllowInsecureAuth = true

	log.Println("Starting server at", s.Addr)
	if err := s.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}


type Backend struct{}

func (bkd *Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
	return &Session{}, nil
}

type Session struct {
	username string
	password string
	from     string
	to       string
}

func (s *Session) AuthPlain(username, password string) error {
	s.username = username
	s.password = password
	return nil
}

func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
	s.from = from
	return nil
}

func (s *Session) Rcpt(to string) error {
	s.to = to
	return nil
}

func (s *Session) Data(r io.Reader) error {
	if b, err := ioutil.ReadAll(r); err != nil {
		return err
	} else {
		httpsProxyURI, _ := url.Parse(os.Getenv("PROXY"))
		httpsDialer, err := proxy.FromURL(httpsProxyURI, HttpsDialer)

		conn, err := httpsDialer.Dial("tcp", targetSMTPServer)
		if err != nil {
			log.Fatalf("Can't socks5Dialer.Dial: %v", err)
		}

		host, _, _ := net.SplitHostPort(targetSMTPServer)

		c, err := smtp.NewClient(conn, host)
		if err != nil {
			log.Fatalf("failed to NewClient as %v", err)
		}

		defer c.Close()

		//c.DebugWriter = os.Stdout

		if err = c.Hello("localhost"); err != nil {
			return err
		}
		if ok, _ := c.Extension("STARTTLS"); ok {
			config := &tls.Config{ServerName: host}
			if err = c.StartTLS(config); err != nil {
				return err
			}
		}

		err = c.Auth(LoginAuth(s.username, s.password))
		if err != nil {
			log.Fatal(err)
		}
		if err = c.Mail(s.from, nil); err != nil {
			return err
		}
		if err = c.Rcpt(s.to); err != nil {
			return err
		}

		w, err := c.Data()
		if err != nil {
			return err
		}
		_, err = w.Write(b)
		if err != nil {
			return err
		}
		err = w.Close()
		if err != nil {
			return err
		}
		return c.Quit()
	}

	return nil
}

func (s *Session) Reset() {}

func (s *Session) Logout() error {
	return nil
}

type loginAuth struct {
	username, password string
}

func LoginAuth(username, password string) sasl.Client {
	return &loginAuth{username, password}
}

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

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

type httpsDialer struct{}

// HTTPSDialer is a https proxy: one that makes network connections on tls.
var HttpsDialer = httpsDialer{}
var TlsConfig = &tls.Config{}

func (d httpsDialer) Dial(network, addr string) (c net.Conn, err error) {
	c, err = tls.Dial("tcp", addr, TlsConfig)
	if err != nil {
		fmt.Println(err)
	}
	return
}

// httpProxy is a HTTP/HTTPS connect proxy.
type httpProxy struct {
	host     string
	haveAuth bool
	username string
	password string
	forward  proxy.Dialer
}

func newHTTPProxy(uri *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
	s := new(httpProxy)
	s.host = uri.Host
	s.forward = forward
	if uri.User != nil {
		s.haveAuth = true
		s.username = uri.User.Username()
		s.password, _ = uri.User.Password()
	}

	return s, nil
}

func (s *httpProxy) Dial(network, addr string) (net.Conn, error) {
	// Dial and create the https client connection.
	c, err := s.forward.Dial("tcp", s.host)
	if err != nil {
		return nil, err
	}

	// HACK. http.ReadRequest also does this.
	reqURL, err := url.Parse("http://" + addr)
	if err != nil {
		c.Close()
		return nil, err
	}
	reqURL.Scheme = ""

	req, err := http.NewRequest("CONNECT", reqURL.String(), nil)
	if err != nil {
		c.Close()
		return nil, err
	}
	req.Close = false
	if s.haveAuth {
		req.SetBasicAuth(s.username, s.password)
		req.Header.Set("Proxy-Authorization", "Basic "+basicAuth(s.username, s.password))
	}

	err = req.Write(c)
	if err != nil {
		c.Close()
		return nil, err
	}

	resp, err := http.ReadResponse(bufio.NewReader(c), req)
	if err != nil {
		// TODO close resp body ?
		resp.Body.Close()
		c.Close()
		return nil, err
	}
	resp.Body.Close()
	if resp.StatusCode != 200 {
		c.Close()
		err = fmt.Errorf("Connect server using proxy error, StatusCode [%d]", resp.StatusCode)
		return nil, err
	}

	return c, nil
}

func init() {
	proxy.RegisterDialerType("http", newHTTPProxy)
	proxy.RegisterDialerType("https", newHTTPProxy)
}

func basicAuth(username, password string) string {
	auth := username + ":" + password
	return base64.StdEncoding.EncodeToString([]byte(auth))
}

So, you can run this code (My current favourite tool is pm2, you can use docker too).

smtp-proxy.config.js:

module.exports = {
    apps: [
        {
            name: 'SMTP-Proxy',
            script: './smtp-proxy',
            autorestart: true,
            env: {
                PROXY: "https://user:password@your-http-connect-proxy.com:443"
            }
        }
    ]
};

It will listen on port 1025. You can configure your service to send emails to port 1025 (disable SMTP SSL/TLS). Then, it will instantly forward the SMTP request to mx1.forwardmx.net:587 (which uses STARTTLS), and forwardmx will send the email for you.

I like the current solution very much. I used to use gotify, ntfy, and even serverchan WeChat push to get reliable notifications, but they are very difficult to set up, and ultimately, no solution is more robust than forwardmx + QQ Mail on mobile + SMTP-proxy.

I hope this helps.