You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aerc/commands/msg/bounce.go

215 lines
5.0 KiB
Go

package msg
import (
"fmt"
"io"
"net/url"
"strings"
"time"
"github.com/emersion/go-message/mail"
"github.com/pkg/errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/commands/mode"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/send"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type Bounce struct {
Account string `opt:"-A" complete:"CompleteAccount" desc:"Account from which to re-send the message."`
To []string `opt:"..." required:"true" complete:"CompleteTo" desc:"Recipient from address book."`
}
func init() {
commands.Register(Bounce{})
}
func (Bounce) Description() string {
return "Re-send the selected message(s) to the specified addresses."
}
func (Bounce) Aliases() []string {
return []string{"bounce", "resend"}
}
func (*Bounce) CompleteAccount(arg string) []string {
return commands.FilterList(app.AccountNames(), arg, commands.QuoteSpace)
}
func (*Bounce) CompleteTo(arg string) []string {
return commands.FilterList(commands.GetAddress(arg), arg, commands.QuoteSpace)
}
func (Bounce) Context() commands.CommandContext {
return commands.MESSAGE_LIST | commands.MESSAGE_VIEWER
}
func (b Bounce) Execute(args []string) error {
if len(b.To) == 0 {
return errors.New("No recipients specified")
}
addresses := strings.Join(b.To, ", ")
app.PushStatus("Bouncing to "+addresses, 10*time.Second)
widget := app.SelectedTabContent().(app.ProvidesMessage)
var err error
acct := widget.SelectedAccount()
if b.Account != "" {
acct, err = app.Account(b.Account)
}
switch {
case err != nil:
return fmt.Errorf("Failed to select account %q: %w", b.Account, err)
case acct == nil:
return errors.New("No account selected")
}
store := widget.Store()
if store == nil {
return errors.New("Cannot perform action. Messages still loading")
}
config := acct.AccountConfig()
outgoing, err := config.Outgoing.ConnectionString()
if err != nil {
return errors.Wrap(err, "ReadCredentials()")
}
if outgoing == "" {
return errors.New("No outgoing mail transport configured for this account")
}
uri, err := url.Parse(outgoing)
if err != nil {
return errors.Wrap(err, "url.Parse()")
}
rcpts, err := mail.ParseAddressList(addresses)
if err != nil {
return errors.Wrap(err, "ParseAddressList()")
}
var domain string
if domain_, ok := config.Params["smtp-domain"]; ok {
domain = domain_
}
hostname, err := send.GetMessageIdHostname(config.SendWithHostname, config.From)
if err != nil {
return errors.Wrap(err, "GetMessageIdHostname()")
}
// According to RFC2822, all of the resent fields corresponding
// to a particular resending of the message SHOULD be together.
// Each new set of resent fields is prepended to the message;
// that is, the most recent set of resent fields appear earlier in the
// message.
headers := fmt.Sprintf("Resent-From: %s\r\n", config.From)
headers += "Resent-Date: %s\r\n"
headers += "Resent-Message-ID: <%s>\r\n"
headers += fmt.Sprintf("Resent-To: %s\r\n", addresses)
helper := newHelper()
uids, err := helper.markedOrSelectedUids()
if err != nil {
return err
}
mode.NoQuit()
marker := store.Marker()
marker.ClearVisualMark()
errCh := make(chan error)
store.FetchFull(uids, func(fm *types.FullMessage) {
defer log.PanicHandler()
var header mail.Header
var msgId string
var err, errClose error
uid := fm.Content.Uid
msg := store.Messages[uid]
if msg == nil {
errCh <- fmt.Errorf("no message info: %v", uid)
return
}
if err = header.GenerateMessageIDWithHostname(hostname); err != nil {
errCh <- errors.Wrap(err, "GenerateMessageIDWithHostname()")
return
}
if msgId, err = header.MessageID(); err != nil {
errCh <- errors.Wrap(err, "MessageID()")
return
}
reader := strings.NewReader(fmt.Sprintf(headers,
time.Now().Format(time.RFC1123Z), msgId))
go func() {
defer log.PanicHandler()
defer func() { errCh <- err }()
var sender io.WriteCloser
log.Debugf("Bouncing email <%s> to %s",
msg.Envelope.MessageId, addresses)
if sender, err = send.NewSender(acct.Worker(), uri,
domain, config.From, rcpts, ""); err != nil {
return
}
defer func() {
errClose = sender.Close()
// If there has already been an error,
// we don't want to clobber it.
if err == nil {
err = errClose
} else if errClose != nil {
app.PushError(errClose.Error())
}
}()
if _, err = io.Copy(sender, reader); err != nil {
return
}
_, err = io.Copy(sender, fm.Content.Reader)
}()
})
go func() {
defer log.PanicHandler()
defer mode.NoQuitDone()
var total, success int
for err = range errCh {
if err != nil {
app.PushError(err.Error())
} else {
success++
}
total++
if total == len(uids) {
break
}
}
if success != total {
marker.Remark()
app.PushError(fmt.Sprintf("Failed to bounce %d of the messages",
total-success))
} else {
plural := ""
if success > 1 {
plural = "s"
}
app.PushStatus(fmt.Sprintf("Bounced %d message%s",
success, plural), 10*time.Second)
}
}()
return nil
}