init: pristine aerc 0.20.0 source

This commit is contained in:
Mortdecai
2026-04-07 19:54:54 -04:00
commit 083402a548
502 changed files with 68722 additions and 0 deletions
+41
View File
@@ -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
}
+52
View File
@@ -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
}
+77
View File
@@ -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
}
+69
View File
@@ -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()
}
+55
View File
@@ -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
}
+134
View File
@@ -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
}