Skip to content

Commit

Permalink
Add option to convert CRLF to LF line endings for sendmail
Browse files Browse the repository at this point in the history
It appears that several versions of sendmail require that the mail is sent to them with
LF line endings instead of CRLF endings - which of course they will then convert back
to CRLF line endings to comply with the SMTP standard.

This PR adds another setting SENDMAIL_CONVERT_CRLF which will pass the message writer
through a filter. This will filter out and convert CRLFs to LFs before writing them
out to sendmail.

Fix go-gitea#18024

Signed-off-by: Andrew Thornton <art27@cantab.net>
  • Loading branch information
zeripath committed Dec 22, 2021
1 parent d097fd6 commit aca9a7b
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 7 deletions.
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,9 @@ PATH =
;;
;; Timeout for Sendmail
;SENDMAIL_TIMEOUT = 5m
;;
;; convert \r\n to \n for Sendmail
;SENDMAIL_CONVERT_CRLF = false

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
1 change: 1 addition & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
command or full path).
- `SENDMAIL_ARGS`: **_empty_**: Specify any extra sendmail arguments.
- `SENDMAIL_TIMEOUT`: **5m**: default timeout for sending email through sendmail
- `SENDMAIL_CONVERT_CRLF`: **false**: some versions of sendmail require LF line endings rather than CRLF line endings. Set this to true if you require this.
- `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]`

## Cache (`cache`)
Expand Down
12 changes: 7 additions & 5 deletions modules/setting/mailer.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ type Mailer struct {
IsTLSEnabled bool

// Sendmail sender
SendmailPath string
SendmailArgs []string
SendmailTimeout time.Duration
SendmailPath string
SendmailArgs []string
SendmailTimeout time.Duration
SendmailConvertCRLF bool
}

var (
Expand Down Expand Up @@ -71,8 +72,9 @@ func newMailService() {
IsTLSEnabled: sec.Key("IS_TLS_ENABLED").MustBool(),
SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""),

SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"),
SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute),
SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"),
SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute),
SendmailConvertCRLF: sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(false),
}
MailService.From = sec.Key("FROM").MustString(MailService.User)
MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("")
Expand Down
80 changes: 78 additions & 2 deletions services/mailer/mailer.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,73 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
return client.Quit()
}

type crlfConverter struct {
danglingCR bool
w io.Writer
}

func (c *crlfConverter) Write(bs []byte) (n int, err error) {
if len(bs) == 0 {
if c.danglingCR {
_, err := c.w.Write([]byte{'\r'})
if err != nil {
return 0, err
}
c.danglingCR = false
}
return c.w.Write(bs)
}
if c.danglingCR {
if bs[0] != '\n' {
_, err := c.w.Write([]byte{'\r'})
if err != nil {
return 0, err
}
}
c.danglingCR = false
}
if bs[len(bs)-1] == '\r' {
c.danglingCR = true
bs = bs[:len(bs)-1]
}
idx := bytes.Index(bs, []byte{'\r', '\n'})
for idx >= 0 {
count, err := c.w.Write(bs[:idx])
n += count
if err != nil {
return n, err
}
count, err = c.w.Write([]byte{'\n'})
if count == 1 {
n += 2
}
if err != nil {
return n, err
}
bs = bs[idx+2:]
idx = bytes.Index(bs, []byte{'\r', '\n'})
}
if len(bs) > 0 {
count, err := c.w.Write(bs)
n += count
if err != nil {
return n, err
}
}
if c.danglingCR {
n++
}
return
}

func (c *crlfConverter) Close() (err error) {
if c.danglingCR {
_, err = c.w.Write([]byte{'\r'})
c.danglingCR = false
}
return
}

// Sender sendmail mail sender
type sendmailSender struct {
}
Expand Down Expand Up @@ -290,13 +357,22 @@ func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error {
return err
}

_, err = msg.WriteTo(pipe)
if setting.MailService.SendmailConvertCRLF {
converter := &crlfConverter{
w: pipe,
}
_, err = msg.WriteTo(converter)
if err == nil {
err = converter.Close()
}
} else {
_, err = msg.WriteTo(pipe)
}

// we MUST close the pipe or sendmail will hang waiting for more of the message
// Also we should wait on our sendmail command even if something fails
closeError = pipe.Close()
waitError = cmd.Wait()

if err != nil {
return err
} else if closeError != nil {
Expand Down
42 changes: 42 additions & 0 deletions services/mailer/mailer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package mailer

import (
"strings"
"testing"
"time"

Expand Down Expand Up @@ -37,3 +38,44 @@ func TestGenerateMessageID(t *testing.T) {
gm = m.ToMessage()
assert.Equal(t, "<msg-d@domain.com>", gm.GetHeader("Message-ID")[0])
}

func TestCRLFConverter(t *testing.T) {
type testcaseType struct {
input []string
expected string
}
testcases := []testcaseType{
{
input: []string{"This h\ras a \r", "\nnewline\r\n"},
expected: "This h\ras a \nnewline\n",
},
{
input: []string{"This\r\n has a \r\n\r", "\n\r\nnewline\r\n"},
expected: "This\n has a \n\n\nnewline\n",
},
{
input: []string{"This has a \r", "\nnewline\r"},
expected: "This has a \nnewline\r",
},
{
input: []string{"This has a \r", "newline\r"},
expected: "This has a \rnewline\r",
},
}
for _, testcase := range testcases {
out := &strings.Builder{}
converter := &crlfConverter{w: out}
realsum, sum := 0, 0
for _, in := range testcase.input {
n, err := converter.Write([]byte(in))
assert.NoError(t, err)
assert.Equal(t, len(in), n)
sum += n
realsum += len(in)
}
err := converter.Close()
assert.NoError(t, err)
assert.Equal(t, realsum, sum)
assert.Equal(t, testcase.expected, out.String())
}
}

0 comments on commit aca9a7b

Please sign in to comment.