这是indexloc提供的服务,不要输入任何密码
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions mail/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/mail"
"net/url"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -211,6 +212,61 @@ func (p *headerParser) parseMsgID() (string, error) {
return left + "@" + right, nil
}

func (p *headerParser) parseListCommand() (*url.URL, error) {
if !p.skipCFWS() {
return nil, errors.New("mail: malformed parenthetical comment")
}

// Consume a potential newline + indent.
p.consume('\r')
p.consume('\n')
p.skipSpace()
Comment on lines +220 to +223
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the input values ever contain newlines. These are stripped by the header parser when unfolding the header fields.


if p.consume('N') && p.consume('O') {
if !p.skipCFWS() {
return nil, errors.New("mail: malformed parenthetical comment")
}

return nil, nil
}

if !p.consume('<') {
return nil, errors.New("mail: missing '<' in list command")
}

i := 0
for p.s[i] != '>' && i+1 < len(p.s) {
i += 1
}
Comment on lines +237 to +240
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can be simplified with strings.IndexByte?


var lit string
lit, p.s = p.s[:i], p.s[i:]

u, err := url.Parse(lit)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC recommends that we remove any whitespace character from the string in-between the angle brackets.

if err != nil {
return u, errors.New("mail: malformed URL")
}

if !p.consume('>') {
return nil, errors.New("mail: missing '>' in list command")
}

if !p.skipCFWS() {
return nil, errors.New("mail: malformed parenthetical comment")
}

// If there isn't a comma, we don't care because it means that there aren't
// any other list command URLs.
p.consume(',')
p.skipSpace()

// Consume a potential newline.
p.consume('\r')
p.consume('\n')

return u, nil
}

// A Header is a mail header.
type Header struct {
message.Header
Expand Down Expand Up @@ -308,6 +364,35 @@ func (h *Header) MsgIDList(key string) ([]string, error) {
return l, nil
}

// MsgIDList parses a list of URLs from a list command header. It returns URLs.
// If the header field is missing, it returns nil.
//
// This can be used on List-Help, List-Unsubscribe, List-Subscribe, List-Post,
// List-Owner, and List-Archive headers.
//
// See https://www.rfc-editor.org/rfc/rfc2369 for more information.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we can just use plaintext to reference the RFC: "See RFC 2369". GoDoc will automatically linkify it, and it's more readable.

//
// In the case that the value of List-Post is the special value, "NO", the
// return value is a slice containing one element, nil.
func (h *Header) ListCommandURLList(key string) ([]*url.URL, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should just use the name ListCommand, and leave out the URLList part.

v := h.Get(key)
if v == "" {
return nil, nil
}

p := headerParser{v}
var l []*url.URL
for !p.empty() {
url, err := p.parseListCommand()
if err != nil {
return l, err
}
l = append(l, url)
}

return l, nil
}

// GenerateMessageID wraps GenerateMessageIDWithHostname and therefore uses the
// hostname of the local machine. This is done to not break existing software.
// Wherever possible better use GenerateMessageIDWithHostname, because the local
Expand Down Expand Up @@ -362,6 +447,18 @@ func (h *Header) SetMsgIDList(key string, l []string) {
}
}

func (h *Header) SetListCommandURLList(key string, urls []*url.URL) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to document this method.

if len(urls) == 0 {
h.Del(key)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to return here, or else we will later on set the header to <>.

}

var ids []string
for _, url := range urls {
ids = append(ids, url.String())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could handle url == nil, and write "NO"?

}
h.Set(key, "<"+strings.Join(ids, ">, <")+">")
}

// Copy creates a stand-alone copy of the header.
func (h *Header) Copy() Header {
return Header{h.Header.Copy()}
Expand Down
222 changes: 222 additions & 0 deletions mail/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
netmail "net/mail"
"net/url"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -215,3 +216,224 @@ func TestHeader_EmptyAddressList(t *testing.T) {
}

}

func TestHeader_ListCommandURLList(t *testing.T) {
tests := []struct {
header string
raw string
urls []*url.URL
xfail bool
}{
{
header: "List-Help",
raw: "<mailto:hello@example.com",
xfail: true,
},
// These tests might seem repetitive, but they are the examples given at
// https://www.rfc-editor.org/rfc/rfc2369.
{
header: "List-Help",
raw: "<mailto:list@host.com?subject=help> (List Instructions)",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list@host.com", RawQuery: "subject=help"},
},
},
{
header: "List-Help",
raw: "<mailto:list-manager@host.com?body=info>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list-manager@host.com", RawQuery: "body=info"},
},
},
{
header: "List-Help",
raw: "<mailto:list-info@host.com> (Info about the list)",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list-info@host.com"},
},
},
{
header: "List-Help",
raw: "<http://www.host.com/list/>, <mailto:list-info@host.com>",
urls: []*url.URL{
{Scheme: "http", Host: "www.host.com", Path: "/list/"},
{Scheme: "mailto", Opaque: "list-info@host.com"},
},
},
{
header: "List-Help",
raw: "<ftp://ftp.host.com/list.txt> (FTP),\r\n\t<mailto:list@host.com?subject=help>",
urls: []*url.URL{
{Scheme: "ftp", Host: "ftp.host.com", Path: "/list.txt"},
{Scheme: "mailto", Opaque: "list@host.com", RawQuery: "subject=help"},
},
},
{
header: "List-Unsubscribe",
raw: "<mailto:list@host.com?subject=unsubscribe>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list@host.com", RawQuery: "subject=unsubscribe"},
},
},
{
header: "List-Unsubscribe",
raw: "(Use this command to get off the list)\r\n\t<mailto:list-manager@host.com?body=unsubscribe%20list>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list-manager@host.com", RawQuery: "body=unsubscribe%20list"},
},
},
{
header: "List-Unsubscribe",
raw: "<mailto:list-off@host.com>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list-off@host.com"},
},
},
{
header: "List-Unsubscribe",
raw: "<http://www.host.com/list.cgi?cmd=unsub&lst=list>,\r\n\t<mailto:list-request@host.com?subject=unsubscribe>",
urls: []*url.URL{
{Scheme: "http", Host: "www.host.com", Path: "/list.cgi", RawQuery: "cmd=unsub&lst=list"},
{Scheme: "mailto", Opaque: "list-request@host.com", RawQuery: "subject=unsubscribe"},
},
},
{
header: "List-Subscribe",
raw: "<mailto:list@host.com?subject=subscribe>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list@host.com", RawQuery: "subject=subscribe"},
},
},
{
header: "List-Subscribe",
raw: "(Use this command to join the list)\r\n\t<mailto:list-manager@host.com?body=subscribe%20list>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list-manager@host.com", RawQuery: "body=subscribe%20list"},
},
},
{
header: "List-Unsubscribe",
raw: "<mailto:list-on@host.com>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list-on@host.com"},
},
},
{
header: "List-Subscribe",
raw: "<http://www.host.com/list.cgi?cmd=sub&lst=list>,\r\n\t<mailto:list-manager@host.com?subject=subscribe>",
urls: []*url.URL{
{Scheme: "http", Host: "www.host.com", Path: "/list.cgi", RawQuery: "cmd=sub&lst=list"},
{Scheme: "mailto", Opaque: "list-manager@host.com", RawQuery: "subject=subscribe"},
},
},
{
header: "List-Post",
raw: "<mailto:list@host.com>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list@host.com"},
},
},
{
header: "List-Post",
raw: "<mailto:moderator@host.com> (Postings are Moderated)",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "moderator@host.com"},
},
},
{
header: "List-Post",
raw: "<mailto:moderator@host.com?subject=list%20posting>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "moderator@host.com", RawQuery: "subject=list%20posting"},
},
},
{
header: "List-Post",
raw: "NO (posting not allowed on this list)",
urls: []*url.URL{nil},
},
{
header: "List-Owner",
raw: "<mailto:listmom@host.com> (Contact Person for Help)",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "listmom@host.com"},
},
},
{
header: "List-Owner",
raw: "<mailto:grant@foo.bar> (Grant Neufeld)",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "grant@foo.bar"},
},
},
{
header: "List-Owner",
raw: "<mailto:josh@foo.bar?Subject=list>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "josh@foo.bar", RawQuery: "Subject=list"},
},
},
{
header: "List-Archive",
raw: "<mailto:archive@host.com?subject=index%20list>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "archive@host.com", RawQuery: "subject=index%20list"},
},
},
{
header: "List-Archive",
raw: "<ftp://ftp.host.com/pub/list/archive/>",
urls: []*url.URL{
{Scheme: "ftp", Host: "ftp.host.com", Path: "/pub/list/archive/"},
},
},
{
header: "List-Archive",
raw: "<http://www.host.com/list/archive/> (Web Archive)",
urls: []*url.URL{
{Scheme: "http", Host: "www.host.com", Path: "/list/archive/"},
},
},
}

for _, test := range tests {
var h mail.Header
h.Set(test.header, test.raw)

urls, err := h.ListCommandURLList(test.header)
if err != nil && !test.xfail {
t.Errorf("Failed to parse %s %q: Header.ListCommandURLList() = %v", test.header, test.raw, err)
} else if !reflect.DeepEqual(urls, test.urls) {
t.Errorf("Failed to parse %s %q: Header.ListCommandURLList() = %q, want %q", test.header, test.raw, urls, test.urls)
}
}
}

func TestHeader_SetListCommandURLList(t *testing.T) {
tests := []struct {
raw string
urls []*url.URL
}{
{
raw: "<mailto:list@example.com>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list@example.com"},
},
},
{
raw: "<mailto:list@example.com>, <https://example.com:8080>",
urls: []*url.URL{
{Scheme: "mailto", Opaque: "list@example.com"},
{Scheme: "https", Host: "example.com:8080"},
},
},
}
for _, test := range tests {
var h mail.Header
h.SetListCommandURLList("List-Post", test.urls)
raw := h.Get("List-Post")
if raw != test.raw {
t.Errorf("Failed to format List-Post %q: Header.Get() = %q, want %q", test.urls, raw, test.raw)
}
}
}