Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing email body when using sendmail #18024

Closed
ben-kenney opened this issue Dec 19, 2021 · 20 comments · Fixed by #18075
Closed

Missing email body when using sendmail #18024

ben-kenney opened this issue Dec 19, 2021 · 20 comments · Fixed by #18075
Labels
issue/needs-feedback For bugs, we need more details. For features, the feature must be described in more detail

Comments

@ben-kenney
Copy link

Gitea Version

1.15.7

Git Version

No response

Operating System

alpine linux (using the official gitea docker image)

How are you running Gitea?

Through the official gitea docker image

Database

PostgreSQL

Can you reproduce the bug on the Gitea demo site?

No

Log Gist

https://gist.github.com/ben-kenney/8fc6715ffd2a6c5721469772957dba2e

Description

I've configured my gitea to use sendmail. I receive email notifications but they are all missing the message body and only contain the subject line.

When I log into my gitea docker container and manually check to see if sendmail is working, I can confirm that sendmail does work BUT it requires a blank line between the subject and the message body, like this:

$ sendmail -S mail.host.com recipient@domain.com
Subject: this is the subject

The message body only appears if it follows a blank line.

The blank line before the message body appears to be important otherwise sendmail will not inlcude the message body, this is documented elsewhere (sorry I couldn't find anything more official than the linked response).

I'm curious to know if this could be the reason why I'm not able to see the email messages from gitea.

I think that emails such as this test email from gitea should likely need \n\n in front of the message body when using sendmail.

return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").ToMessage())

Screenshots

Here's a screenshot of the email that I received from gitea after posting a test comment (note that the message body is blank).

gitea_email

@zeripath
Copy link
Contributor

It would be useful to see your configuration.

  • What are the contents of the [mailer] section in your app.ini?
  • When you say "sendmail" what are you using? (It's likely you're not running sendmail and if you are running sendmail I would strongly advise against that.)

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Dec 19, 2021

It isn't that simple. Can you check the original message of the email you received and post it here?

If you run:

func TestSendmail(t *testing.T) {
	var mailService = setting.Mailer{
		FromEmail: "test@gitea.com",
	}

	setting.MailService = &mailService
	setting.Domain = "localhost"
	m := NewMessage([]string{"test@localhost"}, "Gitea Test Email!", "Gitea Test Email!").ToMessage()
	_, _ = m.WriteTo(os.Stdout)
}

You will find the output, this is what you should see in your original message.

Mime-Version: 1.0
To: test@localhost
Subject: Gitea Test Email!
Date: Sun, 19 Dec 2021 23:36:06 +0800
X-Auto-Response-Suppress: All
Message-ID: <autogen-1639928166163-44d83016484b8890@localhost>
From: test@gitea.com
Content-Type: multipart/alternative;
 boundary=cc97a50fdd9552d3694a5edd3a376972006e278d23aa2be9c97e6ca19512

--cc97a50fdd9552d3694a5edd3a376972006e278d23aa2be9c97e6ca19512
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8

Gitea Test Email!
--cc97a50fdd9552d3694a5edd3a376972006e278d23aa2be9c97e6ca19512
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8

Gitea Test Email!
--cc97a50fdd9552d3694a5edd3a376972006e278d23aa2be9c97e6ca19512--

@wxiaoguang wxiaoguang added the issue/needs-feedback For bugs, we need more details. For features, the feature must be described in more detail label Dec 19, 2021
@zeripath
Copy link
Contributor

... or set "MAILER_TYPE=dummy" in app.ini

@ben-kenney
Copy link
Author

@zeripath Here is my app.ini:

[mailer]
ENABLED = true
FROM = gitea@no-reply.com
MAILER_TYPE = sendmail
SENDMAIL_PATH = /usr/sbin/sendmail
SUBJECT_PREFIX = gitea
SENDMAIL_ARGS = -S my.mail.host
IS_TLS_ENABLED = false
SEND_AS_PLAIN_TEXT = true

when I set MAILER_TYPE = dummy, the logs show this:

2021/12/19 11:32:12 ...ices/auth/session.go:73:SessionUser() [T] Session Authorization: Logged in user 1:gitea-admin
2021/12/19 11:32:12 ...ces/mailer/mailer.go:81:NewMessageFrom() [T] NewMessageFrom (body):
Gitea Test Email!
2021/12/19 11:32:12 ...ces/mailer/mailer.go:295:Send() [I] Mail From: gitea@no-reply.com To: [user@email.com] Body: Mime-Version: 1.0\01503d
Subject: gitea Gitea Test Email!\01503d
Date: Sun, 19 Dec 2021 11:32:12 -0500\01503d
X-Auto-Response-Suppress: All\01503d
From: gitea@no-reply.com\01503d
To: user@email.com\01503d
Content-Type: text/plain; charset=UTF-8\01503d
Content-Transfer-Encoding: quoted-printable\01503d
\01503d
Gitea Test Email!

I'll need to google what \01503d is. If it represents a "blank line" then my theory isn't correct.

Here's the header from the received email that I get (there's no mention of the Gitea Test Email! message body):

MIME-Version: 1.0
From: gitea@no-reply.com
To: user@host.com
Subject: gitea Gitea Test Email!
Date: Sun, 19 Dec 2021 11:36:19 -0500
X-Auto-Response-Suppress: All
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
Message-ID: e4aa4716-2c08-4143-8382-afdc56dca0dc@mail.host.com
Return-Path: gitea@no-reply.com
X-CrossPremisesHeadersFilteredBySendConnector: mail.host.com
X-OrganizationHeadersPreserved: mail.host.com
X-MS-Exchange-Organization-ExpirationStartTime: 19 Dec 2021 16:36:21.2456 (UTC)
...

Not sure if this is the issue but it goes through a MS exchange server. I suspect the MS exchange is not the issue because when I try this manually using the sendmail from the command line from within the docker container I can reproduce this issue (and also confirm that a blank line works).

@zeripath
Copy link
Contributor

zeripath commented Dec 19, 2021

What package is providing the /usr/sbin/sendmail command?

As I said above I'm almost certain that this is not going to be sendmail but rather some other package that provides a sendmail command.

@ben-kenney
Copy link
Author

What package is providing the /use/sbin/sendmail command?

As I said above I'm almost certain that this is not going to be sendmail but rather some other package that provides a sendmail command.

I guess I thought gitea was the package that is providing the sendmail command. There are references to sendmail in the gitea code.

from the logs that I have it looks like it is using sendmail:

2021/12/19 07:04:01 ...ces/mailer/mailer.go:248:Send() [T] Sending with: /usr/sbin/sendmail [-f gitea@no-reply.com -i -S not_showing_mail_host not_showing_useremail@mail.domain]

@zeripath
Copy link
Contributor

$ docker exec -it <gitea_container> /bin/bash
bash-5.1# /usr/sbin/sendmail --help
BusyBox v1.32.1 () multi-call binary.

Usage: sendmail [-tv] [-f SENDER] [-amLOGIN 4<user_pass.txt | -auUSER -apPASS]
		[-w SECS] [-H 'PROG ARGS' | -S HOST] [RECIPIENT_EMAIL]...

Read email from stdin and send it

Standard options:
	-t		Read additional recipients from message body
	-f SENDER	For use in MAIL FROM:<sender>. Can be empty string
			Default: -auUSER, or username of current UID
	-o OPTIONS	Various options. -oi implied, others are ignored
	-i		-oi synonym, implied and ignored

Busybox specific options:
	-v		Verbose
	-w SECS		Network timeout
	-H 'PROG ARGS'	Run connection helper. Examples:
		openssl s_client -quiet -tls1 -starttls smtp -connect smtp.gmail.com:25
		openssl s_client -quiet -tls1 -connect smtp.gmail.com:465
			$SMTP_ANTISPAM_DELAY: seconds to wait after helper connect
	-S HOST[:PORT]	Server (default $SMTPHOST or 127.0.0.1)
	-amLOGIN	Log in using AUTH LOGIN
	-amPLAIN	or AUTH PLAIN
			(-amCRAM-MD5 not supported)
	-auUSER		Username for AUTH
	-apPASS 	Password for AUTH

If no -a options are given, authentication is not done.
If -amLOGIN is given but no -au/-ap, user/password is read from fd #4.
Other options are silently ignored; -oi is implied.
Use makemime to create emails with attachments.

So it's the sendmail provided by busybox

@zeripath
Copy link
Contributor

The \01503d is \015. The 03d is simply due to a bug in the logger which I will fix imminently.

\015 => \r so you can see that the blank line is being passed to "sendmail"

@zeripath
Copy link
Contributor

Not sure if this is the issue but it goes through a MS exchange server. I suspect the MS exchange is not the issue because when I try this manually using the sendmail from the command line from within the docker container I can reproduce this issue (and also confirm that a blank line works).

In your testcase are you sending lines with \r\n?

@ben-kenney
Copy link
Author

First of all, thank you for looking into this! And now I realize what you were asking regarding the package manager.

In your testcase are you sending lines with \r\n?

No, in my test case I was invoking sendmail from the command line, not piping any text to sendmail. I literally just type this:

$ sendmail -S mail.host.com recipient@domain.com
Subject: this is the subject

The message body only appears if it follows a blank line.

What I think is happening is that \n should be used instead of \r, only reason why I say this is because on my linux terminal when I run printf "hello \n world" the output makes a lot more sense to me than printf "hello \r world"

@zeripath
Copy link
Contributor

What I think is happening is that \n should be used instead of \r, only reason why I say this is because on my linux terminal when I run printf "hello \n world" the output makes a lot more sense to me than printf "hello \r world"

The RFC disagrees with you.

Lines have to be terminated with \r\n for SMTP.

@zeripath
Copy link
Contributor

Why aren't you simply using the SMTP mailer directly? If you're just using the sendmail command on the docker AFAICS it's just using SMTP itself and it is not in itself a mailer daemon.

@ben-kenney
Copy link
Author

I started off with SMTP but got autehntication failures. sendmail works for me from the command line and comes close to working
for me with gitea except I don't see the message body.

@zeripath
Copy link
Contributor

zeripath commented Dec 19, 2021

Well it appears your chosen sendmail command doesn't appear to obey the standard. You could try catting a crlf'd message to it to prove that's the problem. If it is then you might have to write a script that will strip out the CRs or...

Given your sendmail command is not doing anything different from what gitea can do I think you should work out why you were getting authentication problems with the SMTP backend or at least give us some more information as to what kind of authentication problems you were getting

@ben-kenney
Copy link
Author

Well it appears your chosen sendmail command doesn't appear to obey the standard.

I'll check to see if I can install a different sendmail from the alpine linux repos.

Given your sendmail command is not doing anything different from what gitea can do I think you should work out why you were getting authentication problems with the SMTP backend

My smtp authentication issues are just that I don't have the credentials to the smtp server and this particular server requires authentication if using MAILER_TYPE = smtp. I do not need to authenticate when using MAILER_TYPE = sendmail which is why I'm using sendmail.

Just to recap,
this works for me when using the sendmail from the gitea docker image:

echo -e "Subject: Subject line\n\nEmail body" | sendmail -S mail.server.com -f "gitea@noreply.com" -t "user@email.com"

this does not work (sends the email but doesn't display message body):
echo -e "Subject:Subject line\r\nEmail body" | sendmail -S mail.server.com -f "gitea@noreply.com" -t "user@email.com"

Thanks for your help today, I guess I'll try to chase down a different sendmail version that isn't packaged by busybox.

@zeripath
Copy link
Contributor

busybox has sed and sed -e 's/\r$//' would remove the carriage returns. If you were to create a script that simply passed the stdin through that before passing it to sendmail that would work

@zeripath
Copy link
Contributor

I don't understand why sendmail does not need to authenticate but gitea would - so I suspect that there is something else odd about your configuration here.

@jprjr
Copy link

jprjr commented Dec 22, 2021

Pretty much all the various Linux/Unix mail daemons internally store messages with just \n line endings, then convert to \r\n as-needed (when sending out over IMAP, relaying via SMTP). So unfortunately, some implementations of sendmail will convert \r\n to plain \n, and some don't.

  • original sendmail: has a flag to convert \r\n to \n when queueing. Converts \n to \r\n for SMTP.
  • qmail: reads input as-is, replaces \n with \r\n for SMTP (so if you use \r\n, it winds up sending \r\r\n to remote servers).
  • postfix: converts \r\n to \n when queueing, see sendmail_fix_line_endings, converts \n to \r\n for SMTP
  • busybox: reads input as-is, converts \n to \r\n when relaying to an SMTP server (similar to qmail)
  • opensmtpd: unknown, I can't find anything in the manual
  • exim: converts \r\n to \n when queueing, see Section 2 - Line Endings, converts back to \r\n for SMTP.
  • msmtp: if the message uses \r\n, it passes them, if the message is just \n, adds \r\n for SMTP

So long-story short, even though the RFC specifies messages should use \r\n, when using sendmail you should use \n line endings for maximum compatibility. They'll all accept a plain \n and convert to \r\n as needed.

@zeripath
Copy link
Contributor

zeripath commented Dec 22, 2021

you could try the following patch and set SENDMAIL_CONVERT_CRLF to true in your app.ini to see if it solves the issue.

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 1d19a3438..987e84a0d 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1494,6 +1494,9 @@ PATH =
 ;;
 ;; Timeout for Sendmail
 ;SENDMAIL_TIMEOUT = 5m
+;;
+;; convert \r\n to \n for Sendmail
+;SENDMAIL_CONVERT_CRLF = false
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 07655a181..00816964a 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -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`)
diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go
index 1bcd63a91..19570ea97 100644
--- a/modules/setting/mailer.go
+++ b/modules/setting/mailer.go
@@ -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 (
@@ -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("")
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index eac2b15c3..da810d380 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -253,6 +253,63 @@ 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 && 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
+}
+
 // Sender sendmail mail sender
 type sendmailSender struct {
 }
@@ -290,13 +347,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 converter.danglingCR && err == nil {
+			_, err = pipe.Write([]byte{'\r'})
+		}
+	} 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 {

zeripath added a commit to zeripath/gitea that referenced this issue Dec 22, 2021
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>
@ben-kenney
Copy link
Author

busybox has sed and sed -e 's/\r$//' would remove the carriage returns. If you were to create a script that simply passed the stdin through that before passing it to sendmail that would work

@zeripath thanks again for all of your help. I was able to implement your suggestion and it worked. I pointed my SENDMAIL_PATH = /path/to/sendmail_wrapper

sendmail_wrapper is just:

#!/bin/sh
sed 's/\r$//' | sendmail -S mail.host.com -vt

6543 pushed a commit that referenced this issue Jan 6, 2022
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 <art27@cantab.net>
Chianina pushed a commit to Chianina/gitea that referenced this issue Mar 28, 2022
…18075)

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>
@go-gitea go-gitea locked and limited conversation to collaborators Apr 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
issue/needs-feedback For bugs, we need more details. For features, the feature must be described in more detail
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants