Add unsubscribe command
The unsubscribe command, available when in a message viewer context, enables users to easily unsubscribe from mailing lists. When the command is executed, aerc looks for a List-Unsubscribe header as defined in RFC 2369. If found, aerc will attempt to present the user with a suitable interface for completing the request. Currently, mailto and http(s) URLs are supported. In the case of a HTTP(S) URL, aerc will open the link in a browser. For mailto links, a new composer tab will be opened with a message filled out according to the URL. The message is not sent automatically in order to provide the user a chance to review it first. Closes #101
This commit is contained in:
parent
1bb1a80156
commit
030f390436
3 changed files with 150 additions and 0 deletions
103
commands/msg/unsubscribe.go
Normal file
103
commands/msg/unsubscribe.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package msg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/lib"
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
)
|
||||
|
||||
// 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")
|
||||
}
|
||||
widget := aerc.SelectedTab().(widgets.ProvidesMessage)
|
||||
headers := widget.SelectedMessage().RFC822Headers
|
||||
if !headers.Has("list-unsubscribe") {
|
||||
return errors.New("No List-Unsubscribe header found")
|
||||
}
|
||||
methods := parseUnsubscribeMethods(headers.Get("list-unsubscribe"))
|
||||
aerc.Logger().Printf("found %d unsubscribe methods", len(methods))
|
||||
for _, method := range methods {
|
||||
aerc.Logger().Printf("trying to unsubscribe using %v", method)
|
||||
switch method.Scheme {
|
||||
case "mailto":
|
||||
return unsubscribeMailto(aerc, method)
|
||||
case "http", "https":
|
||||
return unsubscribeHTTP(method)
|
||||
default:
|
||||
aerc.Logger().Printf("skipping unrecognized scheme: %s", method.Scheme)
|
||||
}
|
||||
}
|
||||
return errors.New("no supported unsubscribe methods found")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
widget := aerc.SelectedTab().(widgets.ProvidesMessage)
|
||||
acct := widget.SelectedAccount()
|
||||
composer := widgets.NewComposer(aerc.Config(), acct.AccountConfig(),
|
||||
acct.Worker())
|
||||
composer.Defaults(map[string]string{
|
||||
"To": u.Opaque,
|
||||
"Subject": u.Query().Get("subject"),
|
||||
})
|
||||
composer.SetContents(strings.NewReader(u.Query().Get("body")))
|
||||
tab := aerc.NewTab(composer, "unsubscribe")
|
||||
composer.OnSubjectChange(func(subject string) {
|
||||
if subject == "" {
|
||||
tab.Name = "unsubscribe"
|
||||
} else {
|
||||
tab.Name = subject
|
||||
}
|
||||
tab.Content.Invalidate()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func unsubscribeHTTP(u *url.URL) error {
|
||||
return lib.OpenFile(u.String())
|
||||
}
|
41
commands/msg/unsubscribe_test.go
Normal file
41
commands/msg/unsubscribe_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package msg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseUnsubscribe(t *testing.T) {
|
||||
type tc struct {
|
||||
hdr string
|
||||
expected []string
|
||||
}
|
||||
cases := []*tc{
|
||||
&tc{"", []string{}},
|
||||
&tc{"invalid", []string{}},
|
||||
&tc{"<https://example.com>, <http://example.com>", []string{
|
||||
"https://example.com", "http://example.com",
|
||||
}},
|
||||
&tc{"<https://example.com> is a URL", []string{
|
||||
"https://example.com",
|
||||
}},
|
||||
&tc{"<mailto:user@host?subject=unsubscribe>, <https://example.com>",
|
||||
[]string{
|
||||
"mailto:user@host?subject=unsubscribe", "https://example.com",
|
||||
}},
|
||||
&tc{"<>, <https://example> ", []string{
|
||||
"", "https://example",
|
||||
}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
result := parseUnsubscribeMethods(c.hdr)
|
||||
if len(result) != len(c.expected) {
|
||||
t.Errorf("expected %d methods but got %d", len(c.expected), len(result))
|
||||
continue
|
||||
}
|
||||
for idx := 0; idx < len(result); idx++ {
|
||||
if result[idx].String() != c.expected[idx] {
|
||||
t.Errorf("expected %v but got %v", c.expected[idx], result[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -85,6 +85,12 @@ message list, the message in the message viewer, etc).
|
|||
*unread*
|
||||
Marks the selected message as unread.
|
||||
|
||||
*unsubscribe*
|
||||
Attempt to automatically unsubscribe the user from the mailing list through
|
||||
use of the List-Unsubscribe header. If supported, aerc may open a compose
|
||||
window pre-filled with the unsubscribe information or open the unsubscribe
|
||||
URL in a web browser.
|
||||
|
||||
## MESSAGE LIST COMMANDS
|
||||
|
||||
*cf* <folder>
|
||||
|
|
Loading…
Reference in a new issue