Skip to content

Commit aca9a7b

Browse files
committed
Add option to convert CRLF to LF line endings for sendmail
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 #18024 Signed-off-by: Andrew Thornton <[email protected]>
1 parent d097fd6 commit aca9a7b

File tree

5 files changed

+131
-7
lines changed

5 files changed

+131
-7
lines changed

custom/conf/app.example.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,9 @@ PATH =
14941494
;;
14951495
;; Timeout for Sendmail
14961496
;SENDMAIL_TIMEOUT = 5m
1497+
;;
1498+
;; convert \r\n to \n for Sendmail
1499+
;SENDMAIL_CONVERT_CRLF = false
14971500

14981501
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14991502
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
667667
command or full path).
668668
- `SENDMAIL_ARGS`: **_empty_**: Specify any extra sendmail arguments.
669669
- `SENDMAIL_TIMEOUT`: **5m**: default timeout for sending email through sendmail
670+
- `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.
670671
- `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]`
671672

672673
## Cache (`cache`)

modules/setting/mailer.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ type Mailer struct {
3737
IsTLSEnabled bool
3838

3939
// Sendmail sender
40-
SendmailPath string
41-
SendmailArgs []string
42-
SendmailTimeout time.Duration
40+
SendmailPath string
41+
SendmailArgs []string
42+
SendmailTimeout time.Duration
43+
SendmailConvertCRLF bool
4344
}
4445

4546
var (
@@ -71,8 +72,9 @@ func newMailService() {
7172
IsTLSEnabled: sec.Key("IS_TLS_ENABLED").MustBool(),
7273
SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""),
7374

74-
SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"),
75-
SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute),
75+
SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"),
76+
SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute),
77+
SendmailConvertCRLF: sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(false),
7678
}
7779
MailService.From = sec.Key("FROM").MustString(MailService.User)
7880
MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("")

services/mailer/mailer.go

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,73 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
253253
return client.Quit()
254254
}
255255

256+
type crlfConverter struct {
257+
danglingCR bool
258+
w io.Writer
259+
}
260+
261+
func (c *crlfConverter) Write(bs []byte) (n int, err error) {
262+
if len(bs) == 0 {
263+
if c.danglingCR {
264+
_, err := c.w.Write([]byte{'\r'})
265+
if err != nil {
266+
return 0, err
267+
}
268+
c.danglingCR = false
269+
}
270+
return c.w.Write(bs)
271+
}
272+
if c.danglingCR {
273+
if bs[0] != '\n' {
274+
_, err := c.w.Write([]byte{'\r'})
275+
if err != nil {
276+
return 0, err
277+
}
278+
}
279+
c.danglingCR = false
280+
}
281+
if bs[len(bs)-1] == '\r' {
282+
c.danglingCR = true
283+
bs = bs[:len(bs)-1]
284+
}
285+
idx := bytes.Index(bs, []byte{'\r', '\n'})
286+
for idx >= 0 {
287+
count, err := c.w.Write(bs[:idx])
288+
n += count
289+
if err != nil {
290+
return n, err
291+
}
292+
count, err = c.w.Write([]byte{'\n'})
293+
if count == 1 {
294+
n += 2
295+
}
296+
if err != nil {
297+
return n, err
298+
}
299+
bs = bs[idx+2:]
300+
idx = bytes.Index(bs, []byte{'\r', '\n'})
301+
}
302+
if len(bs) > 0 {
303+
count, err := c.w.Write(bs)
304+
n += count
305+
if err != nil {
306+
return n, err
307+
}
308+
}
309+
if c.danglingCR {
310+
n++
311+
}
312+
return
313+
}
314+
315+
func (c *crlfConverter) Close() (err error) {
316+
if c.danglingCR {
317+
_, err = c.w.Write([]byte{'\r'})
318+
c.danglingCR = false
319+
}
320+
return
321+
}
322+
256323
// Sender sendmail mail sender
257324
type sendmailSender struct {
258325
}
@@ -290,13 +357,22 @@ func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error {
290357
return err
291358
}
292359

293-
_, err = msg.WriteTo(pipe)
360+
if setting.MailService.SendmailConvertCRLF {
361+
converter := &crlfConverter{
362+
w: pipe,
363+
}
364+
_, err = msg.WriteTo(converter)
365+
if err == nil {
366+
err = converter.Close()
367+
}
368+
} else {
369+
_, err = msg.WriteTo(pipe)
370+
}
294371

295372
// we MUST close the pipe or sendmail will hang waiting for more of the message
296373
// Also we should wait on our sendmail command even if something fails
297374
closeError = pipe.Close()
298375
waitError = cmd.Wait()
299-
300376
if err != nil {
301377
return err
302378
} else if closeError != nil {

services/mailer/mailer_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package mailer
66

77
import (
8+
"strings"
89
"testing"
910
"time"
1011

@@ -37,3 +38,44 @@ func TestGenerateMessageID(t *testing.T) {
3738
gm = m.ToMessage()
3839
assert.Equal(t, "<[email protected]>", gm.GetHeader("Message-ID")[0])
3940
}
41+
42+
func TestCRLFConverter(t *testing.T) {
43+
type testcaseType struct {
44+
input []string
45+
expected string
46+
}
47+
testcases := []testcaseType{
48+
{
49+
input: []string{"This h\ras a \r", "\nnewline\r\n"},
50+
expected: "This h\ras a \nnewline\n",
51+
},
52+
{
53+
input: []string{"This\r\n has a \r\n\r", "\n\r\nnewline\r\n"},
54+
expected: "This\n has a \n\n\nnewline\n",
55+
},
56+
{
57+
input: []string{"This has a \r", "\nnewline\r"},
58+
expected: "This has a \nnewline\r",
59+
},
60+
{
61+
input: []string{"This has a \r", "newline\r"},
62+
expected: "This has a \rnewline\r",
63+
},
64+
}
65+
for _, testcase := range testcases {
66+
out := &strings.Builder{}
67+
converter := &crlfConverter{w: out}
68+
realsum, sum := 0, 0
69+
for _, in := range testcase.input {
70+
n, err := converter.Write([]byte(in))
71+
assert.NoError(t, err)
72+
assert.Equal(t, len(in), n)
73+
sum += n
74+
realsum += len(in)
75+
}
76+
err := converter.Close()
77+
assert.NoError(t, err)
78+
assert.Equal(t, realsum, sum)
79+
assert.Equal(t, testcase.expected, out.String())
80+
}
81+
}

0 commit comments

Comments
 (0)