package contact import ( "bytes" "context" "crypto/sha256" "crypto/tls" "encoding/base64" "encoding/json" "fmt" "io" "log" "net/http" "net/mail" "net/smtp" "net/url" "os" "strings" "time" "github.com/bytbox/go-pop3" "github.com/emersion/go-imap" "github.com/emersion/go-imap/client" "github.com/emersion/go-sasl" "github.com/google/uuid" "golang.org/x/oauth2/authhandler" "golang.org/x/oauth2/google" ) type Emailer struct { From string SMTP string POP3 string IMAP string Password string Limit int OAuth string } func NewEmailer() *Emailer { envOr := func(key, def string) string { if v, ok := os.LookupEnv(strings.ToUpper(key)); ok { return v } return def } return &Emailer{ From: envOr("FROM", "breellocaldev@gmail.com"), SMTP: envOr("SMTP", "smtp.gmail.com:465"), Password: envOr("PASSWORD", "lhnjijrvqaesiufp"), OAuth: envOr("OAUTH", ""), } } func (e *Emailer) Read() (chan *mail.Message, error) { return e.ReadIMAP() } func (e *Emailer) ReadIMAP() (chan *mail.Message, error) { if e.OAuth != "" { return e.oauthThenReadIMAP() } return e.readIMAP(e.From, e.Password) } func (e *Emailer) oauthThenReadIMAP() (chan *mail.Message, error) { configJSON, err := e.configJSON() if err != nil { return nil, err } if token, _ := os.ReadFile(e.OAuth + ".token"); len(token) == 0 { state := uuid.NewString() verifier := uuid.NewString() s256 := sha256.Sum256([]byte(verifier)) challenge := base64.RawURLEncoding.EncodeToString(s256[:]) pkceParams := authhandler.PKCEParams{Challenge: challenge, ChallengeMethod: "S256", Verifier: verifier} config, err := google.ConfigFromJSON([]byte(configJSON), "https://mail.google.com/") if err != nil { return nil, err } credentialsParams := google.CredentialsParams{Scopes: config.Scopes, State: state, AuthHandler: func(authCodeURL string) (string, string, error) { fmt.Println(authCodeURL + "&access_type=offline&prompt=consent") var callback string _, err := fmt.Scan(&callback) u, _ := url.Parse(callback) fmt.Println() return u.Query().Get("code"), state, err }, PKCE: &pkceParams} credentials, err := google.CredentialsFromJSONWithParams(context.Background(), []byte(configJSON), credentialsParams) if err != nil { return nil, err } token, err := credentials.TokenSource.Token() if err != nil { return nil, err } tokenFileJSON, _ := json.Marshal(token) os.WriteFile(e.OAuth+".token", tokenFileJSON, os.ModePerm) log.Printf("%s", tokenFileJSON) } var tokenStruct struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` Expiry time.Time `json:"expiry"` ExpiresIn int `json:"expires_in"` } token, _ := os.ReadFile(e.OAuth + ".token") json.Unmarshal(token, &tokenStruct) if time.Until(tokenStruct.Expiry) < time.Minute*5 { var appStruct struct { Installed struct { ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` GrantType string `json:"grant_type"` RefreshToken string `json:"refresh_token"` } `json:"installed"` } json.Unmarshal([]byte(configJSON), &appStruct) appStruct.Installed.GrantType = "refresh_token" appStruct.Installed.RefreshToken = tokenStruct.RefreshToken b, _ := json.Marshal(appStruct.Installed) resp, err := http.Post(`https://oauth2.googleapis.com/token`, `application/json`, bytes.NewReader(b)) if err != nil { return nil, err } defer resp.Body.Close() b, _ = io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("(%d) %s", resp.StatusCode, b) } tokenStruct.Expiry = time.Time{} json.Unmarshal(b, &tokenStruct) tokenStruct.Expiry = time.Now().Add(time.Duration(tokenStruct.ExpiresIn) * time.Second) tokenStruct.RefreshToken = appStruct.Installed.RefreshToken b, _ = json.Marshal(tokenStruct) os.WriteFile(e.OAuth+".token", b, os.ModePerm) } log.Println("OAUTH", e.From, time.Until(tokenStruct.Expiry), tokenStruct.AccessToken) return e.readIMAP(e.From, tokenStruct.AccessToken) } func (e *Emailer) configJSON() (string, error) { b, err := os.ReadFile(e.OAuth) return string(b), err } func (e *Emailer) readIMAP(authU, authP string) (chan *mail.Message, error) { c, err := client.DialTLS(e.IMAP, nil) if err != nil { return nil, err } if e.OAuth == "" { if err := c.Login(authU, authP); err != nil { return nil, err } } else { if err := c.Authenticate(sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{ Username: authU, Token: authP, Host: "", Port: 0, })); err != nil { return nil, err } } mbox, err := c.Select("INBOX", true) //readonly if err != nil { return nil, err } from := uint32(1) to := mbox.Messages if e.Limit > 0 && to > uint32(e.Limit) { from = to - uint32(e.Limit-1) } seqset := new(imap.SeqSet) seqset.AddRange(from, to) emails := make(chan *mail.Message) messages := make(chan *imap.Message) go func() { defer c.Logout() section := &imap.BodySectionName{} if err := c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()}, messages); err != nil { log.Println("error fetching:", err) } }() go func() { defer close(emails) for msg := range messages { for _, v := range msg.Body { m, err := mail.ReadMessage(v) if err != nil { log.Println(err) } else { emails <- m } } } }() return emails, nil } func (e *Emailer) ReadPOP3() (chan *mail.Message, error) { limit := e.Limit if limit < 1 { limit = 1000 } emails := make(chan *mail.Message, limit) defer close(emails) log.Printf("pop3.DialTLS(%s)", e.POP3) c, err := pop3.DialTLS(e.POP3) if err != nil { return nil, err } defer c.Rset() log.Printf("c.Auth(%s, xyz)", e.From) if err := c.Auth(e.From, e.Password); err != nil { return nil, err } log.Printf("c.ListAll()") ids, _, err := c.ListAll() if err != nil { return nil, err } log.Printf("/c.ListAll() = %v", ids) for i := len(ids) - 1; i >= 0; i-- { id := ids[i] log.Printf("c.Retr(%v)", id) raw, err := c.Retr(id) if err != nil { return nil, err } msg, err := mail.ReadMessage(strings.NewReader(raw)) if err != nil { return nil, err } select { case emails <- msg: default: return emails, nil } } return emails, nil } func (e *Emailer) Send(to, subj, msg string) error { headers := e.headers(to, e.From, subj) body := e.body(headers, msg) smtp, err := e.smtp() if err != nil { return err } defer smtp.Quit() if err := smtp.Mail(e.From); err != nil { return err } if err := smtp.Rcpt(to); err != nil { return err } w, err := smtp.Data() if err != nil { return err } if _, err := w.Write([]byte(body)); err != nil { return err } return w.Close() } func (e *Emailer) headers(to, from, subj string) map[string]string { return map[string]string{ "To": to, "From": from, "Subject": subj, } } func (e *Emailer) body(headers map[string]string, body string) string { b := "" for k, v := range headers { b += fmt.Sprintf("%s: %s\r\n", k, v) } return b + "\r\n" + body } func (e *Emailer) smtp() (*smtp.Client, error) { host := strings.Split(e.SMTP, ":")[0] auth := smtp.PlainAuth("", e.From, e.Password, host) tlsconfig := &tls.Config{ServerName: host} conn, err := tls.Dial("tcp", e.SMTP, tlsconfig) if err != nil { return nil, fmt.Errorf("cannot dial %v: %v", e.SMTP, err) } smtp, err := smtp.NewClient(conn, host) if err != nil { return nil, fmt.Errorf("cannot new client %v: %v", host, err) } if err := smtp.Auth(auth); err != nil { return nil, fmt.Errorf("cannot auth client: %v", err) } return smtp, nil }