2019-07-04 17:01:07 +02:00
|
|
|
package msg
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"errors"
|
2022-05-24 19:12:37 +02:00
|
|
|
"fmt"
|
2019-07-04 17:01:07 +02:00
|
|
|
"net/url"
|
|
|
|
"strings"
|
2022-05-24 19:12:37 +02:00
|
|
|
"time"
|
2019-07-04 17:01:07 +02:00
|
|
|
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/lib"
|
2022-07-19 22:31:51 +02:00
|
|
|
"git.sr.ht/~rjarry/aerc/logging"
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
|
|
"git.sr.ht/~rjarry/aerc/widgets"
|
2020-11-10 20:27:30 +01:00
|
|
|
"github.com/emersion/go-message/mail"
|
2019-07-04 17:01:07 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Unsubscribe helps people unsubscribe from mailing lists by way of the
|
|
|
|
// List-Unsubscribe header.
|
|
|
|
type Unsubscribe struct{}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
register(Unsubscribe{})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Aliases returns a list of aliases for the :unsubscribe command
|
|
|
|
func (Unsubscribe) Aliases() []string {
|
|
|
|
return []string{"unsubscribe"}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Complete returns a list of completions
|
|
|
|
func (Unsubscribe) Complete(aerc *widgets.Aerc, args []string) []string {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Execute runs the Unsubscribe command
|
|
|
|
func (Unsubscribe) Execute(aerc *widgets.Aerc, args []string) error {
|
|
|
|
if len(args) != 1 {
|
|
|
|
return errors.New("Usage: unsubscribe")
|
|
|
|
}
|
2022-07-18 12:54:55 +02:00
|
|
|
widget := aerc.SelectedTabContent().(widgets.ProvidesMessage)
|
2019-07-10 02:04:21 +02:00
|
|
|
msg, err := widget.SelectedMessage()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
headers := msg.RFC822Headers
|
2019-07-04 17:01:07 +02:00
|
|
|
if !headers.Has("list-unsubscribe") {
|
|
|
|
return errors.New("No List-Unsubscribe header found")
|
|
|
|
}
|
2022-02-18 00:34:24 +01:00
|
|
|
text, err := headers.Text("list-unsubscribe")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
methods := parseUnsubscribeMethods(text)
|
2022-05-24 19:12:37 +02:00
|
|
|
if len(methods) == 0 {
|
|
|
|
return fmt.Errorf("no methods found to unsubscribe")
|
|
|
|
}
|
2022-07-19 22:31:51 +02:00
|
|
|
logging.Infof("unsubscribe: found %d methods", len(methods))
|
2022-05-24 19:12:37 +02:00
|
|
|
|
|
|
|
unsubscribe := func(method *url.URL) {
|
2022-07-19 22:31:51 +02:00
|
|
|
logging.Infof("unsubscribe: trying to unsubscribe using %s", method.Scheme)
|
2022-05-24 19:12:37 +02:00
|
|
|
var err error
|
|
|
|
switch strings.ToLower(method.Scheme) {
|
2019-07-04 17:01:07 +02:00
|
|
|
case "mailto":
|
2022-05-24 19:12:37 +02:00
|
|
|
err = unsubscribeMailto(aerc, method)
|
2019-07-04 17:01:07 +02:00
|
|
|
case "http", "https":
|
2022-05-24 19:12:37 +02:00
|
|
|
err = unsubscribeHTTP(aerc, method)
|
2019-07-04 17:01:07 +02:00
|
|
|
default:
|
2022-05-24 19:12:37 +02:00
|
|
|
err = fmt.Errorf("unsubscribe: skipping unrecognized scheme: %s", method.Scheme)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
aerc.PushError(err.Error())
|
2019-07-04 17:01:07 +02:00
|
|
|
}
|
|
|
|
}
|
2022-05-24 19:12:37 +02:00
|
|
|
|
|
|
|
var title string = "Select method to unsubscribe"
|
|
|
|
if msg != nil && msg.Envelope != nil && len(msg.Envelope.From) > 0 {
|
|
|
|
title = fmt.Sprintf("%s from %s", title, msg.Envelope.From[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
options := make([]string, len(methods))
|
|
|
|
for i, method := range methods {
|
|
|
|
options[i] = method.Scheme
|
|
|
|
}
|
|
|
|
|
|
|
|
dialog := widgets.NewSelectorDialog(
|
|
|
|
title,
|
|
|
|
"Press <Enter> to confirm or <ESC> to cancel",
|
|
|
|
options, 0, aerc.SelectedAccountUiConfig(),
|
|
|
|
func(option string, err error) {
|
|
|
|
aerc.CloseDialog()
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, widgets.ErrNoOptionSelected) {
|
|
|
|
aerc.PushStatus("Unsubscribe: "+err.Error(),
|
|
|
|
5*time.Second)
|
|
|
|
} else {
|
|
|
|
aerc.PushError("Unsubscribe: " + err.Error())
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, m := range methods {
|
|
|
|
if m.Scheme == option {
|
|
|
|
unsubscribe(m)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
aerc.PushError("Unsubscribe: selected method not found")
|
|
|
|
},
|
|
|
|
)
|
|
|
|
aerc.AddDialog(dialog)
|
|
|
|
|
|
|
|
return nil
|
2019-07-04 17:01:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// parseUnsubscribeMethods reads the list-unsubscribe header and parses it as a
|
|
|
|
// list of angle-bracket <> deliminated URLs. See RFC 2369.
|
|
|
|
func parseUnsubscribeMethods(header string) (methods []*url.URL) {
|
|
|
|
r := bufio.NewReader(strings.NewReader(header))
|
|
|
|
for {
|
|
|
|
// discard until <
|
|
|
|
_, err := r.ReadSlice('<')
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// read until <
|
|
|
|
m, err := r.ReadSlice('>')
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
m = m[:len(m)-1]
|
|
|
|
if u, err := url.Parse(string(m)); err == nil {
|
|
|
|
methods = append(methods, u)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
|
2022-07-18 12:54:55 +02:00
|
|
|
widget := aerc.SelectedTabContent().(widgets.ProvidesMessage)
|
2019-07-04 17:01:07 +02:00
|
|
|
acct := widget.SelectedAccount()
|
2022-02-25 00:21:06 +01:00
|
|
|
if acct == nil {
|
|
|
|
return errors.New("No account selected")
|
|
|
|
}
|
2020-11-10 20:27:30 +01:00
|
|
|
|
|
|
|
h := &mail.Header{}
|
|
|
|
h.SetSubject(u.Query().Get("subject"))
|
|
|
|
if to, err := mail.ParseAddressList(u.Opaque); err == nil {
|
|
|
|
h.SetAddressList("to", to)
|
2019-07-23 01:29:07 +02:00
|
|
|
}
|
2020-11-10 20:27:30 +01:00
|
|
|
|
2019-11-03 13:51:14 +01:00
|
|
|
composer, err := widgets.NewComposer(
|
2019-09-11 21:28:14 +02:00
|
|
|
aerc,
|
2020-04-24 11:42:21 +02:00
|
|
|
acct,
|
2019-07-23 01:29:07 +02:00
|
|
|
aerc.Config(),
|
|
|
|
acct.AccountConfig(),
|
|
|
|
acct.Worker(),
|
2019-11-03 13:51:14 +01:00
|
|
|
"",
|
2020-11-10 20:27:30 +01:00
|
|
|
h,
|
2020-01-08 21:44:14 +01:00
|
|
|
models.OriginalMail{},
|
2019-07-23 01:29:07 +02:00
|
|
|
)
|
2019-11-03 13:51:14 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-07-04 17:01:07 +02:00
|
|
|
composer.SetContents(strings.NewReader(u.Query().Get("body")))
|
|
|
|
tab := aerc.NewTab(composer, "unsubscribe")
|
2019-07-23 01:29:07 +02:00
|
|
|
composer.OnHeaderChange("Subject", func(subject string) {
|
2019-07-04 17:01:07 +02:00
|
|
|
if subject == "" {
|
|
|
|
tab.Name = "unsubscribe"
|
|
|
|
} else {
|
|
|
|
tab.Name = subject
|
|
|
|
}
|
|
|
|
tab.Content.Invalidate()
|
|
|
|
})
|
2022-05-24 19:12:37 +02:00
|
|
|
composer.FocusTerminal()
|
2019-07-04 17:01:07 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:12:37 +02:00
|
|
|
func unsubscribeHTTP(aerc *widgets.Aerc, u *url.URL) error {
|
|
|
|
confirm := widgets.NewSelectorDialog(
|
|
|
|
"Do you want to open this link?",
|
|
|
|
u.String(),
|
|
|
|
[]string{"No", "Yes"}, 0, aerc.SelectedAccountUiConfig(),
|
2022-07-29 21:11:01 +02:00
|
|
|
func(option string, _ error) {
|
2022-05-24 19:12:37 +02:00
|
|
|
aerc.CloseDialog()
|
|
|
|
switch option {
|
|
|
|
case "Yes":
|
2022-09-30 10:52:49 +02:00
|
|
|
go func() {
|
|
|
|
if err := lib.XDGOpen(u.String()); err != nil {
|
|
|
|
aerc.PushError("Unsubscribe:" + err.Error())
|
|
|
|
}
|
|
|
|
}()
|
2022-05-24 19:12:37 +02:00
|
|
|
default:
|
|
|
|
aerc.PushError("Unsubscribe: link will not be opened")
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
aerc.AddDialog(confirm)
|
|
|
|
return nil
|
2019-07-04 17:01:07 +02:00
|
|
|
}
|