From 62982a9a679e667b7a1b0e7be1adfa3561c7eac4 Mon Sep 17 00:00:00 2001 From: Koni Marti Date: Tue, 24 May 2022 07:36:07 +0200 Subject: invites: reply with accept, accept-tentative or decline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reply to iCalendar invitations with three commands: :accept, :accept-tentative or :decline. Parse a text/calendar request, create a reply and append it to the composer. Suggested-by: Ondřej Synáček Signed-off-by: Koni Marti Acked-by: Robin Jarry --- commands/msg/invite.go | 190 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 commands/msg/invite.go (limited to 'commands') diff --git a/commands/msg/invite.go b/commands/msg/invite.go new file mode 100644 index 0000000..c15e265 --- /dev/null +++ b/commands/msg/invite.go @@ -0,0 +1,190 @@ +package msg + +import ( + "errors" + "fmt" + "io" + + "git.sr.ht/~rjarry/aerc/lib" + "git.sr.ht/~rjarry/aerc/lib/calendar" + "git.sr.ht/~rjarry/aerc/lib/format" + "git.sr.ht/~rjarry/aerc/models" + "git.sr.ht/~rjarry/aerc/widgets" + "github.com/emersion/go-message/mail" +) + +type invite struct{} + +func init() { + register(invite{}) +} + +func (invite) Aliases() []string { + return []string{"accept", "accept-tentative", "decline"} +} + +func (invite) Complete(aerc *widgets.Aerc, args []string) []string { + return nil +} + +func (invite) Execute(aerc *widgets.Aerc, args []string) error { + + acct := aerc.SelectedAccount() + if acct == nil { + return errors.New("no account selected") + } + store := acct.Store() + if store == nil { + return errors.New("cannot perform action: messages still loading") + } + msg, err := acct.SelectedMessage() + if err != nil { + return err + } + + part := lib.FindCalendartext(msg.BodyStructure, nil) + if part == nil { + return fmt.Errorf("no invitation found (missing text/calendar)") + } + + subject := trimLocalizedRe(msg.Envelope.Subject) + switch args[0] { + case "accept": + subject = "Accepted: " + subject + case "accept-tentative": + subject = "Tentatively Accepted: " + subject + case "decline": + subject = "Declined: " + subject + default: + return fmt.Errorf("no participation status defined") + } + + conf := acct.AccountConfig() + from, err := mail.ParseAddress(conf.From) + if err != nil { + return err + } + var aliases []*mail.Address + if conf.Aliases != "" { + aliases, err = mail.ParseAddressList(conf.Aliases) + if err != nil { + return err + } + } + + // figure out the sending from address if we have aliases + if len(aliases) != 0 { + rec := newAddrSet() + rec.AddList(msg.Envelope.To) + rec.AddList(msg.Envelope.Cc) + // test the from first, it has priority over any present alias + if rec.Contains(from) { + // do nothing + } else { + for _, a := range aliases { + if rec.Contains(a) { + from = a + break + } + } + } + } + + var ( + to []*mail.Address + ) + + if len(msg.Envelope.ReplyTo) != 0 { + to = msg.Envelope.ReplyTo + } else { + to = msg.Envelope.From + } + + if !aerc.Config().Compose.ReplyToSelf { + for i, v := range to { + if v.Address == from.Address { + to = append(to[:i], to[i+1:]...) + break + } + } + if len(to) == 0 { + to = msg.Envelope.To + } + } + + recSet := newAddrSet() // used for de-duping + recSet.AddList(to) + + h := &mail.Header{} + h.SetAddressList("from", []*mail.Address{from}) + h.SetSubject(subject) + h.SetMsgIDList("in-reply-to", []string{msg.Envelope.MessageId}) + err = setReferencesHeader(h, msg.RFC822Headers) + if err != nil { + aerc.PushError(fmt.Sprintf("could not set references: %v", err)) + } + original := models.OriginalMail{ + From: format.FormatAddresses(msg.Envelope.From), + Date: msg.Envelope.Date, + RFC822Headers: msg.RFC822Headers, + } + + handleInvite := func(reader io.Reader) (*calendar.Reply, error) { + cr, err := calendar.CreateReply(reader, from, args[0]) + if err != nil { + return nil, err + } + for _, org := range cr.Organizers { + organizer, err := mail.ParseAddress(org) + if err != nil { + continue + } + if !recSet.Contains(organizer) { + to = append(to, organizer) + } + } + h.SetAddressList("to", to) + return cr, nil + } + + addTab := func(cr *calendar.Reply) error { + composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), + acct.AccountConfig(), acct.Worker(), "", h, original) + if err != nil { + aerc.PushError("Error: " + err.Error()) + return err + } + + composer.SetContents(cr.PlainText) + composer.AppendPart(cr.MimeType, cr.Params, cr.CalendarText) + composer.FocusTerminal() + + tab := aerc.NewTab(composer, subject) + composer.OnHeaderChange("Subject", func(subject string) { + if subject == "" { + tab.Name = "New email" + } else { + tab.Name = subject + } + tab.Content.Invalidate() + }) + + composer.OnClose(func(c *widgets.Composer) { + if c.Sent() { + store.Answered([]uint32{msg.Uid}, true, nil) + } + }) + + return nil + } + + store.FetchBodyPart(msg.Uid, part, func(reader io.Reader) { + if cr, err := handleInvite(reader); err != nil { + aerc.PushError(err.Error()) + return + } else { + addTab(cr) + } + }) + return nil +} -- cgit v1.2.3