summaryrefslogtreecommitdiff
path: root/commands
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2022-05-24 07:36:07 +0200
committerRobin Jarry <robin@jarry.cc>2022-05-31 14:32:24 +0200
commit62982a9a679e667b7a1b0e7be1adfa3561c7eac4 (patch)
treec107063eed0bb5ff6d8e7217d6a9c3e44b625c51 /commands
parent6f5c6e148f08266e92875e813105997317bc212b (diff)
downloadaerc-62982a9a679e667b7a1b0e7be1adfa3561c7eac4.zip
invites: reply with accept, accept-tentative or decline
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 <ondrej@synacek.org> Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'commands')
-rw-r--r--commands/msg/invite.go190
1 files changed, 190 insertions, 0 deletions
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
+}