package msg import ( "bufio" "bytes" "errors" "fmt" "io" "math/rand" "os" "path" "strings" "sync" "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/format" "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" "github.com/emersion/go-message/mail" "git.sr.ht/~sircmpwn/getopt" ) type forward struct{} func init() { register(forward{}) } func (forward) Aliases() []string { return []string{"forward"} } func (forward) Complete(aerc *widgets.Aerc, args []string) []string { return nil } func (forward) Execute(aerc *widgets.Aerc, args []string) error { opts, optind, err := getopt.Getopts(args, "AFT:") if err != nil { return err } attachAll := false attachFull := false template := "" for _, opt := range opts { switch opt.Option { case 'A': attachAll = true case 'F': attachFull = true case 'T': template = opt.Value } } if attachAll && attachFull { return errors.New("Options -A and -F are mutually exclusive") } widget := aerc.SelectedTabContent().(widgets.ProvidesMessage) acct := widget.SelectedAccount() if acct == nil { return errors.New("No account selected") } store := widget.Store() if store == nil { return errors.New("Cannot perform action. Messages still loading") } msg, err := widget.SelectedMessage() if err != nil { return err } log.Debugf("Forwarding email <%s>", msg.Envelope.MessageId) h := &mail.Header{} subject := "Fwd: " + msg.Envelope.Subject h.SetSubject(subject) var tolist []*mail.Address to := strings.Join(args[optind:], ", ") if strings.Contains(to, "@") { tolist, err = mail.ParseAddressList(to) if err != nil { return fmt.Errorf("invalid to address(es): %w", err) } } if len(tolist) > 0 { h.SetAddressList("to", tolist) } original := models.OriginalMail{ From: format.FormatAddresses(msg.Envelope.From), Date: msg.Envelope.Date, RFC822Headers: msg.RFC822Headers, } addTab := func() (*widgets.Composer, error) { composer, err := widgets.NewComposer(aerc, acct, acct.AccountConfig(), acct.Worker(), template, h, &original) if err != nil { aerc.PushError("Error: " + err.Error()) return nil, err } tab := aerc.NewTab(composer, subject) if !h.Has("to") { composer.FocusEditor("to") } else { composer.FocusTerminal() } composer.OnHeaderChange("Subject", func(subject string) { if subject == "" { tab.Name = "New email" } else { tab.Name = subject } ui.Invalidate() }) return composer, nil } if attachFull { tmpDir, err := os.MkdirTemp("", "aerc-tmp-attachment") if err != nil { return err } tmpFileName := path.Join(tmpDir, strings.ReplaceAll(fmt.Sprintf("%s.eml", msg.Envelope.Subject), "/", "-")) store.FetchFull([]uint32{msg.Uid}, func(fm *types.FullMessage) { tmpFile, err := os.Create(tmpFileName) if err != nil { log.Warnf("failed to create temporary attachment: %v", err) _, err = addTab() if err != nil { log.Warnf("failed to add tab: %v", err) } return } defer tmpFile.Close() _, err = io.Copy(tmpFile, fm.Content.Reader) if err != nil { log.Warnf("failed to write to tmpfile: %v", err) return } composer, err := addTab() if err != nil { return } composer.AddAttachment(tmpFileName) composer.OnClose(func(_ *widgets.Composer) { os.RemoveAll(tmpDir) }) }) } else { if template == "" { template = config.Templates.Forwards } part := lib.FindPlaintext(msg.BodyStructure, nil) if part == nil { part = lib.FindFirstNonMultipart(msg.BodyStructure, nil) // if it's still nil here, we don't have a multipart msg, that's fine } err = addMimeType(msg, part, &original) if err != nil { return err } store.FetchBodyPart(msg.Uid, part, func(reader io.Reader) { buf := new(bytes.Buffer) scanner := bufio.NewScanner(reader) for scanner.Scan() { buf.WriteString(scanner.Text() + "\n") } original.Text = buf.String() // create composer composer, err := addTab() if err != nil { return } // add attachments if attachAll { var mu sync.Mutex parts := lib.FindAllNonMultipart(msg.BodyStructure, nil, nil) for _, p := range parts { if lib.EqualParts(p, part) { continue } bs, err := msg.BodyStructure.PartAtIndex(p) if err != nil { log.Errorf("cannot get PartAtIndex %v: %v", p, err) continue } store.FetchBodyPart(msg.Uid, p, func(reader io.Reader) { mime := bs.FullMIMEType() params := lib.SetUtf8Charset(bs.Params) name, ok := params["name"] if !ok { name = fmt.Sprintf("%s_%s_%d", bs.MIMEType, bs.MIMESubType, rand.Uint64()) } mu.Lock() err := composer.AddPartAttachment(name, mime, params, reader) mu.Unlock() if err != nil { log.Errorf(err.Error()) aerc.PushError(err.Error()) } }) } } }) } return nil }