]> Cypherpunks repositories - gostls13.git/commitdiff
mail: format addresseses correctly.
authorDavid Symonds <dsymonds@golang.org>
Wed, 8 Jun 2011 03:32:47 +0000 (13:32 +1000)
committerDavid Symonds <dsymonds@golang.org>
Wed, 8 Jun 2011 03:32:47 +0000 (13:32 +1000)
Also remove an obsolete TODO while I'm here.

R=r, rsc
CC=golang-dev
https://golang.org/cl/4588041

src/pkg/mail/message.go
src/pkg/mail/message_test.go

index 50d89d35748efd8a6820ab383c44f481319b40cd..377a8d394385a2149370043d3bb1c71b1e21b759 100644 (file)
@@ -23,7 +23,6 @@ import (
        "log"
        "net/textproto"
        "os"
-       "strconv"
        "strings"
        "time"
 )
@@ -97,8 +96,6 @@ func parseDate(date string) (*time.Time, os.Error) {
        return nil, os.ErrorString("mail: header could not be parsed")
 }
 
-// TODO(dsymonds): Parsers for more specific headers such as To, From, etc.
-
 // A Header represents the key-value pairs in a mail message header.
 type Header map[string][]string
 
@@ -136,12 +133,50 @@ type Address struct {
        Address string // user@domain
 }
 
+// String formats the address as a valid RFC 5322 address.
+// If the address's name contains non-ASCII characters
+// the name will be rendered according to RFC 2047.
 func (a *Address) String() string {
        s := "<" + a.Address + ">"
        if a.Name == "" {
                return s
        }
-       return "\"" + strconv.Quote(a.Name) + "\" " + s
+       // If every character is printable ASCII, quoting is simple.
+       allPrintable := true
+       for i := 0; i < len(a.Name); i++ {
+               if !isVchar(a.Name[i]) {
+                       allPrintable = false
+                       break
+               }
+       }
+       if allPrintable {
+               b := bytes.NewBufferString(`"`)
+               for i := 0; i < len(a.Name); i++ {
+                       if !isQtext(a.Name[i]) {
+                               b.WriteByte('\\')
+                       }
+                       b.WriteByte(a.Name[i])
+               }
+               b.WriteString(`" `)
+               b.WriteString(s)
+               return b.String()
+       }
+
+       // UTF-8 "Q" encoding
+       b := bytes.NewBufferString("=?utf-8?q?")
+       for i := 0; i < len(a.Name); i++ {
+               switch c := a.Name[i]; {
+               case c == ' ':
+                       b.WriteByte('_')
+               case isVchar(c) && c != '=' && c != '?' && c != '_':
+                       b.WriteByte(c)
+               default:
+                       fmt.Fprintf(b, "=%02X", c)
+               }
+       }
+       b.WriteString("?= ")
+       b.WriteString(s)
+       return b.String()
 }
 
 type addrParser []byte
@@ -327,7 +362,7 @@ Loop:
                        }
                        qsb = append(qsb, (*p)[i+1])
                        i += 2
-               case '!' <= c && c <= '~', c == ' ' || c == '\t':
+               case isQtext(c), c == ' ' || c == '\t':
                        // qtext (printable US-ASCII excluding " and \), or
                        // FWS (almost; we're ignoring CRLF)
                        qsb = append(qsb, c)
@@ -392,3 +427,18 @@ func isAtext(c byte, dot bool) bool {
        }
        return bytes.IndexByte(atextChars, c) >= 0
 }
+
+// isQtext returns true if c is an RFC 5322 qtest character.
+func isQtext(c byte) bool {
+       // Printable US-ASCII, excluding backslash or quote.
+       if c == '\\' || c == '"' {
+               return false
+       }
+       return '!' <= c && c <= '~'
+}
+
+// isVchar returns true if c is an RFC 5322 VCHAR character.
+func isVchar(c byte) bool {
+       // Visible (printing) characters.
+       return '!' <= c && c <= '~'
+}
index c3ec236816a5589b971e389956498704af687ba5..731a748edecb14c1f72ea0c735f54f79be1c1a5e 100644 (file)
@@ -198,3 +198,30 @@ func TestAddressParsing(t *testing.T) {
                }
        }
 }
+
+func TestAddressFormatting(t *testing.T) {
+       tests := []struct {
+               addr *Address
+               exp  string
+       }{
+               {
+                       &Address{Address: "bob@example.com"},
+                       "<bob@example.com>",
+               },
+               {
+                       &Address{Name: "Bob", Address: "bob@example.com"},
+                       `"Bob" <bob@example.com>`,
+               },
+               {
+                       // note the ö (o with an umlaut)
+                       &Address{Name: "Böb", Address: "bob@example.com"},
+                       `=?utf-8?q?B=C3=B6b?= <bob@example.com>`,
+               },
+       }
+       for _, test := range tests {
+               s := test.addr.String()
+               if s != test.exp {
+                       t.Errorf("Address%+v.String() = %v, want %v", *test.addr, s, test.exp)
+               }
+       }
+}