311 lines
7.6 KiB
Go
Executable File
311 lines
7.6 KiB
Go
Executable File
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
|
|
Inbox 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", ""),
|
|
Inbox: envOr("INBOX", "INBOX"),
|
|
}
|
|
}
|
|
|
|
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(e.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
|
|
}
|