Skip to content

Instantly share code, notes, and snippets.

@murakmii
Created January 5, 2020 09:39
Show Gist options
  • Save murakmii/939b6b9eef4ab040e471ae69bca9ecb6 to your computer and use it in GitHub Desktop.
Save murakmii/939b6b9eef4ab040e471ae69bca9ecb6 to your computer and use it in GitHub Desktop.
SMTPでお喋り
package main
import (
"bufio"
"crypto/tls"
"encoding/base64"
"fmt"
"strconv"
"strings"
)
const (
gmailUsername = "<username>"
gmailPassword = "<password>"
sendTo = "<mail-address>"
)
type (
smtpClient struct {
conn *tls.Conn
reader *bufio.Reader
handler smtpReplyHandler
}
smtpReply struct {
Code int
Messages []string
}
smtpReplyHandler func(*smtpReply) []byte
)
func connectToSMTPServer(host string, handler smtpReplyHandler) (*smtpClient, error) {
conn, err := tls.Dial("tcp", host+":465", &tls.Config{})
if err != nil {
return nil, fmt.Errorf("failed to connect smtp server: %s", err)
}
return &smtpClient{
conn: conn,
reader: bufio.NewReader(conn),
handler: handler,
}, nil
}
func (sc *smtpClient) startTransaction() error {
var continued *smtpReply
for {
rawReply, err := sc.reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read reply from smtp server: %s", err)
}
reply, multi, err := parseReply(strings.TrimSuffix(rawReply, "\r\n"))
if err != nil {
return fmt.Errorf("smtp server replied wrong: %s", err)
}
if continued != nil {
if continued.Code != reply.Code {
return fmt.Errorf("smtp server replied wrong(code mismatch: %d <=> %d)", continued.Code, reply.Code)
}
reply.Messages = append(continued.Messages, reply.Messages...)
}
if multi {
continued = reply
continue
} else {
continued = nil
}
fmt.Printf("[server] --> [client] %s\n", reply)
if reply.Code == 221 {
fmt.Printf("smtp server replied 221. transaction finished\n")
return nil
}
msg := sc.handler(reply)
if len(msg) == 0 {
continue
}
fmt.Printf("[server] <-- [client] %s", string(msg))
if _, err = sc.conn.Write(msg); err != nil {
return fmt.Errorf("failed to write: %s", err)
}
}
}
func (sc *smtpClient) close() error {
return sc.conn.Close()
}
func parseReply(rawReply string) (*smtpReply, bool, error) {
code, err := strconv.ParseInt(rawReply[0:3], 10, 31)
if err != nil {
return nil, false, fmt.Errorf("invalid code: %s", err)
}
if code < 100 || code > 599 {
return nil, false, fmt.Errorf("unsupported code: %d", code)
}
reply := &smtpReply{
Code: int(code),
Messages: strings.Split(rawReply[4:], " "),
}
multi := rawReply[3] == '-'
return reply, multi, nil
}
func (sr *smtpReply) String() string {
return fmt.Sprintf("%d %s", sr.Code, strings.Join(sr.Messages, " "))
}
func main() {
cmdWhenOK := []string{
"AUTH PLAIN\r\n",
fmt.Sprintf("MAIL FROM:<%[email protected]>\r\n", gmailUsername),
fmt.Sprintf("RCPT TO:<%s>\r\n", sendTo),
"DATA\r\n",
"QUIT\r\n",
}
client, err := connectToSMTPServer("smtp.gmail.com", func(reply *smtpReply) []byte {
switch reply.Code {
case 220:
return []byte("EHLO gmail.com\r\n")
case 235, 250:
msg := cmdWhenOK[0]
cmdWhenOK = cmdWhenOK[1:]
return []byte(msg)
case 334:
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("\x00%s\x00%s", gmailUsername, gmailPassword)))
return []byte(auth + "\r\n")
case 354:
mail := fmt.Sprintf(`From: %[email protected]
To: %s
Subject: test from go
Hello!
.
`, gmailUsername, sendTo)
return []byte(strings.ReplaceAll(mail, "\n", "\r\n"))
default:
return []byte("QUIT\r\n")
}
})
if err != nil {
panic(err)
}
defer client.close()
if err := client.startTransaction(); err != nil {
panic(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment