diff --git a/webmail/message.go b/webmail/message.go index 1128371..f9fb77e 100644 --- a/webmail/message.go +++ b/webmail/message.go @@ -45,7 +45,7 @@ func formatFirstLine(r io.Reader) (string, error) { ensureLines() isSnipped := func(s string) bool { - return s == "[...]" || s == "..." + return s == "[...]" || s == "[…]" || s == "..." } nextLineQuoted := func(i int) bool { @@ -55,7 +55,8 @@ func formatFirstLine(r io.Reader) (string, error) { return i+1 < len(lines) && (strings.HasPrefix(lines[i+1], ">") || isSnipped(lines[i+1])) } - // remainder is signature if we see a line with only and minimum 2 dashes, and there are no more empty lines, and there aren't more than 5 lines left + // Remainder is signature if we see a line with only and minimum 2 dashes, and + // there are no more empty lines, and there aren't more than 5 lines left. isSignature := func() bool { if len(lines) == 0 || !strings.HasPrefix(lines[0], "--") || strings.Trim(strings.TrimSpace(lines[0]), "-") != "" { return false @@ -77,6 +78,10 @@ func formatFirstLine(r io.Reader) (string, error) { result := "" + resultSnipped := func() bool { + return strings.HasSuffix(result, "[...]\n") || strings.HasSuffix(result, "[…]") + } + // Quick check for initial wrapped "On ... wrote:" line. if len(lines) > 3 && strings.HasPrefix(lines[0], "On ") && !strings.HasSuffix(lines[0], "wrote:") && strings.HasSuffix(lines[1], ":") && nextLineQuoted(1) { result = "[...]\n" @@ -87,7 +92,7 @@ func formatFirstLine(r io.Reader) (string, error) { for ; len(lines) > 0 && !isSignature(); ensureLines() { line := lines[0] if strings.HasPrefix(line, ">") { - if !strings.HasSuffix(result, "[...]\n") { + if !resultSnipped() { result += "[...]\n" } lines = lines[1:] @@ -101,14 +106,14 @@ func formatFirstLine(r io.Reader) (string, error) { // line, with an optional empty line in between. If we don't have any text yet, we // don't require the digits. if strings.HasSuffix(line, ":") && (strings.ContainsAny(line, "0123456789") || result == "") && nextLineQuoted(0) { - if !strings.HasSuffix(result, "[...]\n") { + if !resultSnipped() { result += "[...]\n" } lines = lines[1:] continue } - // Skip snipping by author. - if !(isSnipped(line) && strings.HasSuffix(result, "[...]\n")) { + // Skip possibly duplicate snipping by author. + if !isSnipped(line) || !resultSnipped() { result += line + "\n" } lines = lines[1:] diff --git a/webmail/message_test.go b/webmail/message_test.go index 3d107ab..e7d275a 100644 --- a/webmail/message_test.go +++ b/webmail/message_test.go @@ -1,11 +1,39 @@ package webmail import ( + "strings" "testing" "github.com/mjl-/mox/dns" ) +func TestFormatFirstLine(t *testing.T) { + check := func(body, expLine string) { + t.Helper() + + line, err := formatFirstLine(strings.NewReader(body)) + tcompare(t, err, nil) + if line != expLine { + t.Fatalf("got %q, expected %q, for body %q", line, expLine, body) + } + } + + check("", "") + check("single line", "single line\n") + check("single line\n", "single line\n") + check("> quoted\n", "[...]\n") + check("> quoted\nresponse\n", "[...]\nresponse\n") + check("> quoted\n[...]\nresponse after author snip\n", "[...]\nresponse after author snip\n") + check("[...]\nresponse after author snip\n", "[...]\nresponse after author snip\n") + check("[…]\nresponse after author snip\n", "[…]\nresponse after author snip\n") + check(">> quoted0\n> quoted1\n>quoted2\n[...]\nresponse after author snip\n", "[...]\nresponse after author snip\n") + check(">quoted\n\n>quoted\ncoalesce line-separated quotes\n", "[...]\ncoalesce line-separated quotes\n") + check("On wrote:\n> hi\nresponse", "[...]\nresponse\n") + check("On \n wrote:\n> hi\nresponse", "[...]\nresponse\n") + check("> quote\nresponse\n--\nsignature\n", "[...]\nresponse\n") + check("> quote\nline1\nline2\nline3\n", "[...]\nline1\nline2\nline3\n") +} + func TestParseListPostAddress(t *testing.T) { check := func(s string, exp *MessageAddress) { t.Helper()