Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
56fd0ba
readme: restrict CI badge to master branch
emersion Feb 13, 2023
67aae7b
Upgrade dependencies
emersion Mar 11, 2023
31d7ac9
go fmt
emersion Jun 15, 2023
4a149f7
message: add note about charset used when writing new messages
emersion Jun 15, 2023
f80b76e
charset: support x-UTF_8J
balejk Jul 21, 2023
ea300ca
readme: switch back to pkg.go.dev for docs
emersion Aug 15, 2023
7716c61
Fix unused variable
emersion Aug 27, 2023
8138ff9
Upgrade dependencies
emersion Aug 27, 2023
27d78e3
Upgrade golang.org/x/text
emersion Jan 10, 2024
e2b84a6
go fmt
emersion Jan 10, 2024
b207ff1
ci: add gofmt check
emersion Jan 10, 2024
ee3ebaf
ci: switch to alpine/latest
emersion Jan 10, 2024
ae3be30
mail: improve test name for Header.Date CFWS
emersion Jan 10, 2024
07f7264
mail: return zero time on missing Date field
emersion Jan 10, 2024
ee2a79e
mail: unset Message-Id if empty
emersion Jan 10, 2024
de399d8
mail: fix dropped error
alrs Jan 25, 2024
c2ff70f
Add more robust line wrapper
emersion Mar 2, 2024
dbb628d
message: drop extraneous newline
emersion Mar 6, 2024
f7e55c4
message: handle Writer.Close errors in Entity.WriteTo
emersion Mar 6, 2024
acf41e6
readme: drop CI badge
emersion Jul 13, 2024
6a718fa
Fixup line wrapper short write bug
agcom Sep 28, 2024
10f5046
mail: use sealed interface for PartHeader
emersion Dec 22, 2024
46f63d4
message: document that an Entity can be consumed at most once
emersion Dec 22, 2024
5336cde
message, textproto: use sealed interfaces for HeaderFields
emersion Jan 3, 2025
162c134
message: check that parsed messages can be reformatted verbatim (#187)
rjarry Jan 13, 2025
b9039e0
textproto: fix typo in the doc for Header.AddRaw method
pawelz Feb 16, 2025
54ce9e7
feat: remove non-ascii header key prefix
CampGareth Jan 11, 2023
2e71243
fix: go mod
CampGareth Jan 11, 2023
8f2c3c0
fix: message.go
CampGareth Jan 11, 2023
24a646a
fix/remove_special_characters
omerijaz27 Jan 16, 2023
4939db3
feat: renovate configurations
Feb 23, 2023
245e3ed
Merge branch 'master' of https://github.com/mirrorweb/go-message
fcuenca4 Apr 29, 2025
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
5 changes: 4 additions & 1 deletion .build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
image: alpine/edge
image: alpine/latest
packages:
- go
sources:
Expand All @@ -15,3 +15,6 @@ tasks:
- coverage: |
cd go-message
go tool cover -html=coverage.txt -o ~/coverage.html
- gofmt: |
cd go-message
test -z $(gofmt -l .)
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# go-message

[![godocs.io](https://godocs.io/github.com/emersion/go-message?status.svg)](https://godocs.io/github.com/emersion/go-message)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-message/commits.svg)](https://builds.sr.ht/~emersion/go-message/commits?)
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-message.svg)](https://pkg.go.dev/github.com/emersion/go-message)

A Go library for the Internet Message Format. It implements:

Expand Down
2 changes: 2 additions & 0 deletions charset/charset.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/ianaindex"
"golang.org/x/text/encoding/unicode"
)

// Quirks table for charsets not handled by ianaindex
Expand All @@ -24,6 +25,7 @@ import (
// https://www.iana.org/assignments/character-sets/character-sets.xhtml
var charsets = map[string]encoding.Encoding{
"ansi_x3.110-1983": charmap.ISO8859_1, // see RFC 1345 page 62, mostly superset of ISO 8859-1
"x-utf_8j": unicode.UTF8, // alias for UTF-8, see https://icu4c-demos.unicode.org/icu-bin/convexp?s=ALL
}

func init() {
Expand Down
75 changes: 71 additions & 4 deletions encoding.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package message

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"mime/quotedprintable"
"strings"

"github.com/emersion/go-textwrapper"
)

type UnknownEncodingError struct {
Expand Down Expand Up @@ -57,9 +56,9 @@ func encodingWriter(enc string, w io.Writer) (io.WriteCloser, error) {
case "quoted-printable":
wc = quotedprintable.NewWriter(w)
case "base64":
wc = base64.NewEncoder(base64.StdEncoding, textwrapper.NewRFC822(w))
wc = base64.NewEncoder(base64.StdEncoding, &lineWrapper{w: w, maxLineLen: 76})
case "7bit", "8bit":
wc = nopCloser{textwrapper.New(w, "\r\n", 1000)}
wc = nopCloser{&lineWrapper{w: w, maxLineLen: 998}}
case "binary", "":
wc = nopCloser{w}
default:
Expand All @@ -86,3 +85,71 @@ func (r *whitespaceReplacingReader) Read(p []byte) (int, error) {

return n, err
}

type lineWrapper struct {
w io.Writer
maxLineLen int

curLineLen int
cr bool
}

func (w *lineWrapper) Write(b []byte) (int, error) {
var written int
for len(b) > 0 {
var l []byte
l, b = cutLine(b, w.maxLineLen-w.curLineLen)

lf := bytes.HasSuffix(l, []byte("\n"))
l = bytes.TrimSuffix(l, []byte("\n"))

n, err := w.w.Write(l)
if err != nil {
return written, err
}
written += n

cr := bytes.HasSuffix(l, []byte("\r"))
if len(l) == 0 {
cr = w.cr
}

if !lf && len(b) == 0 {
w.curLineLen += len(l)
w.cr = cr
break
}
w.curLineLen = 0

ending := []byte("\r\n")
if cr {
ending = []byte("\n")
}
_, err = w.w.Write(ending)
if err != nil {
return written, err
}
// If the written `\n` was part of the input bytes slice, then account for it.
if lf {
written++
}
w.cr = false
}

return written, nil
}

func cutLine(b []byte, max int) ([]byte, []byte) {
for i := 0; i < len(b); i++ {
if b[i] == '\r' && i == max {
continue
}
if b[i] == '\n' {
return b[:i+1], b[i+1:]
}
if i >= max {
return b[:i], b[i:]
}
}
return b, nil
}
96 changes: 96 additions & 0 deletions encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,99 @@ func TestEncode(t *testing.T) {
}
}
}

var lineWrapperTests = []struct {
name string
in, out string
}{
{
name: "empty",
in: "",
out: "",
},
{
name: "oneLine",
in: "ab",
out: "ab",
},
{
name: "oneLineMax",
in: "abc",
out: "abc",
},
{
name: "twoLines",
in: "abcde",
out: "abc\r\nde",
},
{
name: "twoLinesMax",
in: "abcdef",
out: "abc\r\ndef",
},
{
name: "threeLines",
in: "abcdefhi",
out: "abc\r\ndef\r\nhi",
},
{
name: "wrappedMiss",
in: "abcd\nef",
out: "abc\r\nd\r\nef",
},
{
name: "wrappedLF",
in: "abc\ndef\nhij",
out: "abc\r\ndef\r\nhij",
},
{
name: "wrappedCRLF",
in: "abc\r\ndef\r\nhij",
out: "abc\r\ndef\r\nhij",
},
{
name: "trailingCRLF",
in: "a\r\n",
out: "a\r\n",
},
{
name: "cr",
in: "\r\r\r\r\r",
out: "\r\r\r\r\n\r",
},
}

func TestLineWrapper(t *testing.T) {
for _, tc := range lineWrapperTests {
t.Run(tc.name, func(t *testing.T) {
var sb strings.Builder
w := &lineWrapper{w: &sb, maxLineLen: 3}
if _, err := io.WriteString(w, tc.in); err != nil {
t.Fatalf("WriteString() = %v", err)
}
if s := sb.String(); s != tc.out {
t.Errorf("got %q, want %q", s, tc.out)
}
})

t.Run(tc.name+"/bytePerByte", func(t *testing.T) {
var sb strings.Builder
w := &lineWrapper{w: &sb, maxLineLen: 3}
if err := writeStringBytePerByte(w, tc.in); err != nil {
t.Fatalf("writeStringBytePerByte() = %v", err)
}
if s := sb.String(); s != tc.out {
t.Errorf("got %q, want %q", s, tc.out)
}
})
}
}

func writeStringBytePerByte(w io.Writer, s string) error {
for i := 0; i < len(s); i++ {
if _, err := w.Write([]byte{s[i]}); err != nil {
return err
}
}
return nil
}
11 changes: 9 additions & 2 deletions entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (

// An Entity is either a whole message or a one of the parts in the body of a
// multipart entity.
//
// An Entity can only be consumed once: after its body is read, it can't be
// used anymore.
type Entity struct {
Header Header // The entity's header.
Body io.Reader // The decoded entity's body.
Expand Down Expand Up @@ -187,9 +190,13 @@ func (e *Entity) WriteTo(w io.Writer) error {
if err != nil {
return err
}
defer ew.Close()

return e.writeBodyTo(ew)
if err := e.writeBodyTo(ew); err != nil {
ew.Close()
return err
}

return ew.Close()
}

// WalkFunc is the type of the function called for each part visited by Walk.
Expand Down
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ module github.com/mirrorweb/go-message

go 1.14

require (
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594
golang.org/x/text v0.3.7
)
require golang.org/x/text v0.14.0
33 changes: 30 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
4 changes: 4 additions & 0 deletions header.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@ type HeaderFields interface {
// charset is decoded to UTF-8. If the header field's charset is unknown,
// the raw field value is returned and the error verifies IsUnknownCharset.
Text() (string, error)

headerFields()
}

type headerFields struct {
textproto.HeaderFields
}

func (*headerFields) headerFields() {}

func (hf *headerFields) Text() (string, error) {
return decodeHeader(hf.Value())
}
Expand Down
4 changes: 4 additions & 0 deletions mail/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ type AttachmentHeader struct {
message.Header
}

var _ PartHeader = (*AttachmentHeader)(nil)

func (*AttachmentHeader) partHeader() {}

// Filename parses the attachment's filename.
func (h *AttachmentHeader) Filename() (string, error) {
_, params, err := h.ContentDisposition()
Expand Down
27 changes: 20 additions & 7 deletions mail/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@ func (p *headerParser) parseMsgID() (string, error) {
right, err = p.parseNoFoldLiteral()
} else {
right, err = p.parseAtomText(true)
if err != nil {
return "", err
}
}
if err != nil {
return "", err
}

if !p.consume('>') {
Expand Down Expand Up @@ -253,14 +253,23 @@ func (h *Header) SetAddressList(key string, addrs []*Address) {
}
}

// Date parses the Date header field.
// Date parses the Date header field. If the header field is missing, it
// returns the zero time.
func (h *Header) Date() (time.Time, error) {
return mail.ParseDate(h.Get("Date"))
v := h.Get("Date")
if v == "" {
return time.Time{}, nil
}
return mail.ParseDate(v)
}

// SetDate formats the Date header field.
func (h *Header) SetDate(t time.Time) {
h.Set("Date", t.Format(dateLayout))
if !t.IsZero() {
h.Set("Date", t.Format(dateLayout))
} else {
h.Del("Date")
}
}

// Subject parses the Subject header field. If there is an error, the raw field
Expand Down Expand Up @@ -350,7 +359,11 @@ func base36(input uint64) string {
// SetMessageID sets the Message-ID field. id is the message identifier,
// without the angle brackets.
func (h *Header) SetMessageID(id string) {
h.Set("Message-Id", "<"+id+">")
if id != "" {
h.Set("Message-Id", "<"+id+">")
} else {
h.Del("Message-Id")
}
}

// SetMsgIDList formats a list of message identifiers. Message identifiers
Expand Down
Loading