init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/emersion/go-message/mail"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
)
|
||||
|
||||
func newJmapSender(
|
||||
worker *types.Worker, from *mail.Address, rcpts []*mail.Address,
|
||||
copyTo []string,
|
||||
) (io.WriteCloser, error) {
|
||||
var writer io.WriteCloser
|
||||
done := make(chan error)
|
||||
|
||||
worker.PostAction(
|
||||
&types.StartSendingMessage{From: from, Rcpts: rcpts, CopyTo: copyTo},
|
||||
func(msg types.WorkerMessage) {
|
||||
switch msg := msg.(type) {
|
||||
case *types.Done:
|
||||
return
|
||||
case *types.Unsupported:
|
||||
done <- fmt.Errorf("unsupported by worker")
|
||||
case *types.Error:
|
||||
done <- msg.Error
|
||||
case *types.MessageWriter:
|
||||
writer = msg.Writer
|
||||
default:
|
||||
done <- fmt.Errorf("unexpected worker message: %#v", msg)
|
||||
}
|
||||
close(done)
|
||||
},
|
||||
)
|
||||
|
||||
err := <-done
|
||||
|
||||
return writer, err
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
func parseScheme(uri *url.URL) (protocol string, auth string, err error) {
|
||||
protocol = ""
|
||||
auth = "plain"
|
||||
if uri.Scheme != "" {
|
||||
parts := strings.Split(uri.Scheme, "+")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
protocol = parts[0]
|
||||
case 2:
|
||||
if parts[1] == "insecure" {
|
||||
protocol = uri.Scheme
|
||||
} else {
|
||||
protocol = parts[0]
|
||||
auth = parts[1]
|
||||
}
|
||||
case 3:
|
||||
protocol = parts[0] + "+" + parts[1]
|
||||
auth = parts[2]
|
||||
default:
|
||||
return "", "", fmt.Errorf("Unknown scheme %s", uri.Scheme)
|
||||
}
|
||||
}
|
||||
return protocol, auth, nil
|
||||
}
|
||||
|
||||
func GetMessageIdHostname(sendWithHostname bool, from *mail.Address) (string, error) {
|
||||
if sendWithHostname {
|
||||
return os.Hostname()
|
||||
}
|
||||
if from == nil {
|
||||
// no from address present, generate a random hostname
|
||||
return strings.ToUpper(strconv.FormatInt(rand.Int63(), 36)), nil
|
||||
}
|
||||
_, domain, found := strings.Cut(from.Address, "@")
|
||||
if !found {
|
||||
return "", fmt.Errorf("Invalid address %q", from)
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib"
|
||||
)
|
||||
|
||||
func newSaslClient(auth string, uri *url.URL) (sasl.Client, error) {
|
||||
var saslClient sasl.Client
|
||||
switch auth {
|
||||
case "":
|
||||
fallthrough
|
||||
case "none":
|
||||
saslClient = nil
|
||||
case "login":
|
||||
password, _ := uri.User.Password()
|
||||
saslClient = sasl.NewLoginClient(uri.User.Username(), password)
|
||||
case "plain":
|
||||
password, _ := uri.User.Password()
|
||||
saslClient = sasl.NewPlainClient("", uri.User.Username(), password)
|
||||
case "oauthbearer":
|
||||
q := uri.Query()
|
||||
oauth2 := &oauth2.Config{}
|
||||
if q.Get("token_endpoint") != "" {
|
||||
oauth2.ClientID = q.Get("client_id")
|
||||
oauth2.ClientSecret = q.Get("client_secret")
|
||||
oauth2.Scopes = []string{q.Get("scope")}
|
||||
oauth2.Endpoint.TokenURL = q.Get("token_endpoint")
|
||||
}
|
||||
password, _ := uri.User.Password()
|
||||
bearer := lib.OAuthBearer{
|
||||
OAuth2: oauth2,
|
||||
Enabled: true,
|
||||
}
|
||||
if bearer.OAuth2.Endpoint.TokenURL != "" {
|
||||
token, err := bearer.ExchangeRefreshToken(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password = token.AccessToken
|
||||
}
|
||||
saslClient = sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
|
||||
Username: uri.User.Username(),
|
||||
Token: password,
|
||||
})
|
||||
case "xoauth2":
|
||||
q := uri.Query()
|
||||
oauth2 := &oauth2.Config{}
|
||||
if q.Get("token_endpoint") != "" {
|
||||
oauth2.ClientID = q.Get("client_id")
|
||||
oauth2.ClientSecret = q.Get("client_secret")
|
||||
oauth2.Scopes = []string{q.Get("scope")}
|
||||
oauth2.Endpoint.TokenURL = q.Get("token_endpoint")
|
||||
}
|
||||
password, _ := uri.User.Password()
|
||||
bearer := lib.Xoauth2{
|
||||
OAuth2: oauth2,
|
||||
Enabled: true,
|
||||
}
|
||||
if bearer.OAuth2.Endpoint.TokenURL != "" {
|
||||
token, err := bearer.ExchangeRefreshToken(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password = token.AccessToken
|
||||
}
|
||||
saslClient = lib.NewXoauth2Client(uri.User.Username(), password)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported auth mechanism %s", auth)
|
||||
}
|
||||
return saslClient, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/emersion/go-message/mail"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
)
|
||||
|
||||
// NewSender returns an io.WriterCloser into which the caller can write
|
||||
// contents of a message. The caller must invoke the Close() method on the
|
||||
// sender when finished.
|
||||
func NewSender(
|
||||
worker *types.Worker, uri *url.URL, domain string,
|
||||
from *mail.Address, rcpts []*mail.Address,
|
||||
copyTo []string,
|
||||
) (io.WriteCloser, error) {
|
||||
protocol, auth, err := parseScheme(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var w io.WriteCloser
|
||||
|
||||
switch protocol {
|
||||
case "smtp", "smtp+insecure", "smtps":
|
||||
w, err = newSmtpSender(protocol, auth, uri, domain, from, rcpts)
|
||||
case "jmap":
|
||||
w, err = newJmapSender(worker, from, rcpts, copyTo)
|
||||
case "":
|
||||
w, err = newSendmailSender(uri, rcpts)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported protocol %s", protocol)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &crlfWriter{w: w}, nil
|
||||
}
|
||||
|
||||
type crlfWriter struct {
|
||||
w io.WriteCloser
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *crlfWriter) Write(p []byte) (int, error) {
|
||||
return w.buf.Write(p)
|
||||
}
|
||||
|
||||
func (w *crlfWriter) Close() error {
|
||||
defer w.w.Close() // ensure closed even on error
|
||||
|
||||
scan := bufio.NewScanner(&w.buf)
|
||||
for scan.Scan() {
|
||||
if _, err := w.w.Write(append(scan.Bytes(), '\r', '\n')); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if scan.Err() != nil {
|
||||
return scan.Err()
|
||||
}
|
||||
|
||||
return w.w.Close()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
|
||||
"git.sr.ht/~rjarry/go-opt/v2"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type sendmailSender struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
}
|
||||
|
||||
func (s *sendmailSender) Write(p []byte) (int, error) {
|
||||
return s.stdin.Write(p)
|
||||
}
|
||||
|
||||
func (s *sendmailSender) Close() error {
|
||||
se := s.stdin.Close()
|
||||
ce := s.cmd.Wait()
|
||||
if se != nil {
|
||||
return se
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
func newSendmailSender(uri *url.URL, rcpts []*mail.Address) (io.WriteCloser, error) {
|
||||
args := opt.SplitArgs(uri.Path)
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("no command specified")
|
||||
}
|
||||
bin := args[0]
|
||||
rs := make([]string, len(rcpts))
|
||||
for i := range rcpts {
|
||||
rs[i] = rcpts[i].Address
|
||||
}
|
||||
args = append(args[1:], rs...)
|
||||
cmd := exec.Command(bin, args...)
|
||||
s := &sendmailSender{cmd: cmd}
|
||||
var err error
|
||||
s.stdin, err = s.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cmd.StdinPipe")
|
||||
}
|
||||
err = s.cmd.Start()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cmd.Start")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func connectSmtp(starttls bool, host string, domain string) (*smtp.Client, error) {
|
||||
serverName := host
|
||||
if !strings.ContainsRune(host, ':') {
|
||||
host += ":587" // Default to submission port
|
||||
} else {
|
||||
serverName = host[:strings.IndexRune(host, ':')]
|
||||
}
|
||||
var conn *smtp.Client
|
||||
var err error
|
||||
if starttls {
|
||||
conn, err = smtp.DialStartTLS(host, &tls.Config{ServerName: serverName})
|
||||
} else {
|
||||
conn, err = smtp.Dial(host)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "smtp.Dial")
|
||||
}
|
||||
if domain != "" {
|
||||
err := conn.Hello(domain)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, errors.Wrap(err, "Hello")
|
||||
}
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func connectSmtps(host string, domain string) (*smtp.Client, error) {
|
||||
serverName := host
|
||||
if !strings.ContainsRune(host, ':') {
|
||||
host += ":465" // Default to smtps port
|
||||
} else {
|
||||
serverName = host[:strings.IndexRune(host, ':')]
|
||||
}
|
||||
conn, err := smtp.DialTLS(host, &tls.Config{
|
||||
ServerName: serverName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "smtp.DialTLS")
|
||||
}
|
||||
if domain != "" {
|
||||
err := conn.Hello(domain)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, errors.Wrap(err, "Hello")
|
||||
}
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
type smtpSender struct {
|
||||
conn *smtp.Client
|
||||
w io.WriteCloser
|
||||
}
|
||||
|
||||
func (s *smtpSender) Write(p []byte) (int, error) {
|
||||
return s.w.Write(p)
|
||||
}
|
||||
|
||||
func (s *smtpSender) Close() error {
|
||||
we := s.w.Close()
|
||||
ce := s.conn.Close()
|
||||
if we != nil {
|
||||
return we
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
func newSmtpSender(
|
||||
protocol string, auth string, uri *url.URL, domain string,
|
||||
from *mail.Address, rcpts []*mail.Address,
|
||||
) (io.WriteCloser, error) {
|
||||
var err error
|
||||
var conn *smtp.Client
|
||||
switch protocol {
|
||||
case "smtp":
|
||||
conn, err = connectSmtp(true, uri.Host, domain)
|
||||
case "smtp+insecure":
|
||||
conn, err = connectSmtp(false, uri.Host, domain)
|
||||
case "smtps":
|
||||
conn, err = connectSmtps(uri.Host, domain)
|
||||
default:
|
||||
return nil, fmt.Errorf("not a smtp protocol %s", protocol)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Connection failed")
|
||||
}
|
||||
|
||||
saslclient, err := newSaslClient(auth, uri)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if saslclient != nil {
|
||||
if err := conn.Auth(saslclient); err != nil {
|
||||
conn.Close()
|
||||
return nil, errors.Wrap(err, "conn.Auth")
|
||||
}
|
||||
}
|
||||
s := &smtpSender{
|
||||
conn: conn,
|
||||
}
|
||||
if err := s.conn.Mail(from.Address, nil); err != nil {
|
||||
conn.Close()
|
||||
return nil, errors.Wrap(err, "conn.Mail")
|
||||
}
|
||||
for _, rcpt := range rcpts {
|
||||
if err := s.conn.Rcpt(rcpt.Address, nil); err != nil {
|
||||
conn.Close()
|
||||
return nil, errors.Wrap(err, "conn.Rcpt")
|
||||
}
|
||||
}
|
||||
s.w, err = s.conn.Data()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, errors.Wrap(err, "conn.Data")
|
||||
}
|
||||
return s.w, nil
|
||||
}
|
||||
Reference in New Issue
Block a user