mirror of
https://git.sr.ht/~sircmpwn/aerc
synced 2025-12-27 09:31:39 +01:00
Prior to this commit, the composer was based on a map[string]string. While this approach was very versatile, it lead to a constant encoding / decoding of addresses and other headers. This commit switches to a different model, where the composer is based on a header. Commands which want to interact with it can simply set some defaults they would like to have. Users can overwrite them however they like. In order to get access to the functions generating / getting the msgid go-message was upgraded.
326 lines
7.5 KiB
Go
326 lines
7.5 KiB
Go
package compose
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/emersion/go-sasl"
|
|
"github.com/emersion/go-smtp"
|
|
"github.com/google/shlex"
|
|
"github.com/miolini/datacounter"
|
|
"github.com/pkg/errors"
|
|
|
|
"git.sr.ht/~sircmpwn/aerc/lib"
|
|
"git.sr.ht/~sircmpwn/aerc/lib/format"
|
|
"git.sr.ht/~sircmpwn/aerc/models"
|
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
|
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
|
"github.com/emersion/go-message/mail"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
type Send struct{}
|
|
|
|
func init() {
|
|
register(Send{})
|
|
}
|
|
|
|
func (Send) Aliases() []string {
|
|
return []string{"send"}
|
|
}
|
|
|
|
func (Send) Complete(aerc *widgets.Aerc, args []string) []string {
|
|
return nil
|
|
}
|
|
|
|
func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
|
if len(args) > 1 {
|
|
return errors.New("Usage: send")
|
|
}
|
|
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
|
config := composer.Config()
|
|
|
|
if config.Outgoing == "" {
|
|
return errors.New(
|
|
"No outgoing mail transport configured for this account")
|
|
}
|
|
|
|
aerc.Logger().Println("Sending mail")
|
|
|
|
uri, err := url.Parse(config.Outgoing)
|
|
if err != nil {
|
|
return errors.Wrap(err, "url.Parse(outgoing)")
|
|
}
|
|
var (
|
|
scheme string
|
|
auth string = "plain"
|
|
)
|
|
if uri.Scheme != "" {
|
|
parts := strings.Split(uri.Scheme, "+")
|
|
if len(parts) == 1 {
|
|
scheme = parts[0]
|
|
} else if len(parts) == 2 {
|
|
scheme = parts[0]
|
|
auth = parts[1]
|
|
} else {
|
|
return fmt.Errorf("Unknown transfer protocol %s", uri.Scheme)
|
|
}
|
|
}
|
|
|
|
header, err := composer.PrepareHeader()
|
|
if err != nil {
|
|
return errors.Wrap(err, "PrepareHeader")
|
|
}
|
|
rcpts, err := listRecipients(header)
|
|
if err != nil {
|
|
return errors.Wrap(err, "listRecipients")
|
|
}
|
|
|
|
if config.From == "" {
|
|
return errors.New("No 'From' configured for this account")
|
|
}
|
|
from, err := format.ParseAddress(config.From)
|
|
if err != nil {
|
|
return errors.Wrap(err, "ParseAddress(config.From)")
|
|
}
|
|
|
|
var (
|
|
saslClient sasl.Client
|
|
conn *smtp.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 == "" {
|
|
return fmt.Errorf("No 'TokenURL' configured for this account")
|
|
}
|
|
token, err := bearer.ExchangeRefreshToken(password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
password = token.AccessToken
|
|
|
|
saslClient = sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
|
|
Username: uri.User.Username(),
|
|
Token: password,
|
|
})
|
|
default:
|
|
return fmt.Errorf("Unsupported auth mechanism %s", auth)
|
|
}
|
|
|
|
aerc.RemoveTab(composer)
|
|
|
|
var starttls bool
|
|
if starttls_, ok := config.Params["smtp-starttls"]; ok {
|
|
starttls = starttls_ == "yes"
|
|
}
|
|
|
|
smtpAsync := func() (int, error) {
|
|
switch scheme {
|
|
case "smtp":
|
|
host := uri.Host
|
|
serverName := uri.Host
|
|
if !strings.ContainsRune(host, ':') {
|
|
host = host + ":587" // Default to submission port
|
|
} else {
|
|
serverName = host[:strings.IndexRune(host, ':')]
|
|
}
|
|
conn, err = smtp.Dial(host)
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "smtp.Dial")
|
|
}
|
|
defer conn.Close()
|
|
if sup, _ := conn.Extension("STARTTLS"); sup {
|
|
if !starttls {
|
|
err := errors.New("STARTTLS is supported by this server, " +
|
|
"but not set in accounts.conf. " +
|
|
"Add smtp-starttls=yes")
|
|
return 0, err
|
|
}
|
|
if err = conn.StartTLS(&tls.Config{
|
|
ServerName: serverName,
|
|
}); err != nil {
|
|
return 0, errors.Wrap(err, "StartTLS")
|
|
}
|
|
} else {
|
|
if starttls {
|
|
err := errors.New("STARTTLS requested, but not supported " +
|
|
"by this SMTP server. Is someone tampering with your " +
|
|
"connection?")
|
|
return 0, err
|
|
}
|
|
}
|
|
case "smtps":
|
|
host := uri.Host
|
|
serverName := uri.Host
|
|
if !strings.ContainsRune(host, ':') {
|
|
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 0, errors.Wrap(err, "smtp.DialTLS")
|
|
}
|
|
defer conn.Close()
|
|
}
|
|
|
|
if saslClient != nil {
|
|
if err = conn.Auth(saslClient); err != nil {
|
|
return 0, errors.Wrap(err, "conn.Auth")
|
|
}
|
|
}
|
|
// TODO: the user could conceivably want to use a different From and sender
|
|
if err = conn.Mail(from.Address, nil); err != nil {
|
|
return 0, errors.Wrap(err, "conn.Mail")
|
|
}
|
|
aerc.Logger().Printf("rcpt to: %v", rcpts)
|
|
for _, rcpt := range rcpts {
|
|
if err = conn.Rcpt(rcpt); err != nil {
|
|
return 0, errors.Wrap(err, "conn.Rcpt")
|
|
}
|
|
}
|
|
wc, err := conn.Data()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "conn.Data")
|
|
}
|
|
defer wc.Close()
|
|
ctr := datacounter.NewWriterCounter(wc)
|
|
composer.WriteMessage(header, ctr)
|
|
return int(ctr.Count()), nil
|
|
}
|
|
|
|
sendmailAsync := func() (int, error) {
|
|
args, err := shlex.Split(uri.Path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(args) == 0 {
|
|
return 0, fmt.Errorf("no command specified")
|
|
}
|
|
bin := args[0]
|
|
args = append(args[1:], rcpts...)
|
|
cmd := exec.Command(bin, args...)
|
|
wc, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "cmd.StdinPipe")
|
|
}
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "cmd.Start")
|
|
}
|
|
ctr := datacounter.NewWriterCounter(wc)
|
|
composer.WriteMessage(header, ctr)
|
|
wc.Close() // force close to make sendmail send
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "cmd.Wait")
|
|
}
|
|
return int(ctr.Count()), nil
|
|
}
|
|
|
|
sendAsync := func() (int, error) {
|
|
fmt.Println(scheme)
|
|
switch scheme {
|
|
case "smtp":
|
|
fallthrough
|
|
case "smtps":
|
|
return smtpAsync()
|
|
case "":
|
|
return sendmailAsync()
|
|
}
|
|
return 0, errors.New("Unknown scheme")
|
|
}
|
|
|
|
go func() {
|
|
aerc.PushStatus("Sending...", 10*time.Second)
|
|
nbytes, err := sendAsync()
|
|
if err != nil {
|
|
aerc.PushError(" " + err.Error())
|
|
return
|
|
}
|
|
if config.CopyTo != "" {
|
|
aerc.PushStatus("Copying to "+config.CopyTo, 10*time.Second)
|
|
worker := composer.Worker()
|
|
r, w := io.Pipe()
|
|
worker.PostAction(&types.AppendMessage{
|
|
Destination: config.CopyTo,
|
|
Flags: []models.Flag{models.SeenFlag},
|
|
Date: time.Now(),
|
|
Reader: r,
|
|
Length: nbytes,
|
|
}, func(msg types.WorkerMessage) {
|
|
switch msg := msg.(type) {
|
|
case *types.Done:
|
|
aerc.PushStatus("Message sent.", 10*time.Second)
|
|
r.Close()
|
|
composer.SetSent()
|
|
composer.Close()
|
|
case *types.Error:
|
|
aerc.PushError(" " + msg.Error.Error())
|
|
r.Close()
|
|
composer.Close()
|
|
}
|
|
})
|
|
header, err := composer.PrepareHeader()
|
|
if err != nil {
|
|
aerc.PushError(" " + err.Error())
|
|
w.Close()
|
|
return
|
|
}
|
|
composer.WriteMessage(header, w)
|
|
w.Close()
|
|
} else {
|
|
aerc.PushStatus("Message sent.", 10*time.Second)
|
|
composer.SetSent()
|
|
composer.Close()
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func listRecipients(h *mail.Header) ([]string, error) {
|
|
var rcpts []string
|
|
for _, key := range []string{"to", "cc", "bcc"} {
|
|
list, err := h.AddressList(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, addr := range list {
|
|
rcpts = append(rcpts, addr.Address)
|
|
}
|
|
}
|
|
return rcpts, nil
|
|
}
|