mirror of https://git.sr.ht/~rjarry/aerc
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.
215 lines
5.0 KiB
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
|
|
}
|