mirror of
				https://github.com/binwiederhier/ntfy.git
				synced 2025-10-31 13:02:24 +01:00 
			
		
		
		
	Parse nested multipart emails, fixes #610
This commit is contained in:
		
							parent
							
								
									e9b05e8ed7
								
							
						
					
					
						commit
						5f75e98861
					
				
					 3 changed files with 90 additions and 26 deletions
				
			
		|  | @ -6,7 +6,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release | |||
| 
 | ||||
| **Bug fixes + maintenance:** | ||||
| 
 | ||||
| * Support for base64 encoded emails ([#610](https://github.com/binwiederhier/ntfy/issues/610), thanks to [@Robert-litts](https://github.com/Robert-litts)) | ||||
| * Support for base64-encoded and nested multipart emails ([#610](https://github.com/binwiederhier/ntfy/issues/610), thanks to [@Robert-litts](https://github.com/Robert-litts)) | ||||
| 
 | ||||
| **Additional languages:** | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,9 +22,14 @@ var ( | |||
| 	errInvalidAddress         = errors.New("invalid address") | ||||
| 	errInvalidTopic           = errors.New("invalid topic") | ||||
| 	errTooManyRecipients      = errors.New("too many recipients") | ||||
| 	errMultipartNestedTooDeep = errors.New("multipart message nested too deep") | ||||
| 	errUnsupportedContentType = errors.New("unsupported content type") | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	maxMultipartDepth = 2 | ||||
| ) | ||||
| 
 | ||||
| // smtpBackend implements SMTP server methods. | ||||
| type smtpBackend struct { | ||||
| 	config  *Config | ||||
|  | @ -121,7 +126,7 @@ func (s *smtpSession) Data(r io.Reader) error { | |||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		body, err := readMailBody(msg) | ||||
| 		body, err := readMailBody(msg.Body, msg.Header) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | @ -203,36 +208,42 @@ func (s *smtpSession) withFailCount(fn func() error) error { | |||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func readMailBody(msg *mail.Message) (string, error) { | ||||
| 	if msg.Header.Get("Content-Type") == "" { | ||||
| 		return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding")) | ||||
| func readMailBody(body io.Reader, header mail.Header) (string, error) { | ||||
| 	if header.Get("Content-Type") == "" { | ||||
| 		return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding")) | ||||
| 	} | ||||
| 	contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type")) | ||||
| 	contentType, params, err := mime.ParseMediaType(header.Get("Content-Type")) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if strings.ToLower(contentType) == "text/plain" { | ||||
| 		return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding")) | ||||
| 		return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding")) | ||||
| 	} else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") { | ||||
| 		return readMultipartMailBody(msg, params) | ||||
| 		return readMultipartMailBody(body, params, 0) | ||||
| 	} | ||||
| 	return "", errUnsupportedContentType | ||||
| } | ||||
| 
 | ||||
| func readMultipartMailBody(msg *mail.Message, params map[string]string) (string, error) { | ||||
| 	mr := multipart.NewReader(msg.Body, params["boundary"]) | ||||
| func readMultipartMailBody(body io.Reader, params map[string]string, depth int) (string, error) { | ||||
| 	if depth >= maxMultipartDepth { | ||||
| 		return "", errMultipartNestedTooDeep | ||||
| 	} | ||||
| 	mr := multipart.NewReader(body, params["boundary"]) | ||||
| 	for { | ||||
| 		part, err := mr.NextPart() | ||||
| 		if err != nil { // may be io.EOF | ||||
| 			return "", err | ||||
| 		} | ||||
| 		partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) | ||||
| 		partContentType, partParams, err := mime.ParseMediaType(part.Header.Get("Content-Type")) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} else if strings.ToLower(partContentType) != "text/plain" { | ||||
| 			continue | ||||
| 		} | ||||
| 		if strings.ToLower(partContentType) == "text/plain" { | ||||
| 			return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding")) | ||||
| 		} else if strings.HasPrefix(strings.ToLower(partContentType), "multipart/") { | ||||
| 			return readMultipartMailBody(part, partParams, depth+1) | ||||
| 		} | ||||
| 		// Continue with next part | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -390,10 +390,8 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== | |||
| 	writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	func TestSmtpBackend_NestedMultipartBase64(t *testing.T) { | ||||
| func TestSmtpBackend_NestedMultipartBase64(t *testing.T) { | ||||
| 	email := `EHLO example.com | ||||
| 
 | ||||
| MAIL FROM: test@mydomain.me | ||||
| RCPT TO: ntfy-mytopic@ntfy.sh | ||||
| DATA | ||||
|  | @ -429,6 +427,61 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== | |||
| 
 | ||||
| --===============2138658284696597373==-- | ||||
| . | ||||
| ` | ||||
| 
 | ||||
| 	s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) { | ||||
| 		require.Equal(t, "/mytopic", r.URL.Path) | ||||
| 		require.Equal(t, "TrueNAS truenas.local: TrueNAS Test Message hostname: truenas.local", r.Header.Get("Title")) | ||||
| 		require.Equal(t, "This is a test message from TrueNAS CORE.", readAll(t, r.Body)) | ||||
| 	}) | ||||
| 	defer s.Close() | ||||
| 	defer c.Close() | ||||
| 	writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") | ||||
| } | ||||
| 
 | ||||
| func TestSmtpBackend_NestedMultipartTooDeep(t *testing.T) { | ||||
| 	email := `EHLO example.com | ||||
| MAIL FROM: test@mydomain.me | ||||
| RCPT TO: ntfy-mytopic@ntfy.sh | ||||
| DATA | ||||
| Content-Type: multipart/mixed; boundary="===============1==" | ||||
| MIME-Version: 1.0 | ||||
| Subject: TrueNAS truenas.local: TrueNAS Test Message hostname: truenas.local | ||||
| From: =?utf-8?q?Robbie?= <test@mydomain.me> | ||||
| To: test@mydomain.me | ||||
| Date: Thu, 16 Feb 2023 01:04:00 -0000 | ||||
| Message-ID: <truenas-20230216.010400.344514.b'8jfL'@truenas.local> | ||||
| 
 | ||||
| This is a multi-part message in MIME format. | ||||
| --===============1== | ||||
| Content-Type: multipart/alternative; boundary="===============2==" | ||||
| MIME-Version: 1.0 | ||||
| 
 | ||||
| --===============2== | ||||
| Content-Type: multipart/alternative; boundary="===============3==" | ||||
| MIME-Version: 1.0 | ||||
| 
 | ||||
| --===============3== | ||||
| Content-Type: text/plain; charset="utf-8" | ||||
| MIME-Version: 1.0 | ||||
| Content-Transfer-Encoding: base64 | ||||
| 
 | ||||
| VGhpcyBpcyBhIHRlc3QgbWVzc2FnZSBmcm9tIFRydWVOQVMgQ09SRS4= | ||||
| 
 | ||||
| --===============3== | ||||
| Content-Type: text/html; charset="utf-8" | ||||
| MIME-Version: 1.0 | ||||
| Content-Transfer-Encoding: base64 | ||||
| 
 | ||||
| PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMCBUcmFuc2l0aW9uYWwv | ||||
| L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== | ||||
| 
 | ||||
| --===============3==-- | ||||
| 
 | ||||
| --===============2==-- | ||||
| 
 | ||||
| --===============1==-- | ||||
| . | ||||
| ` | ||||
| 
 | ||||
| 	s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) { | ||||
|  | @ -436,9 +489,9 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== | |||
| 	}) | ||||
| 	defer s.Close() | ||||
| 	defer c.Close() | ||||
| 		writeAndReadUntilLine(t, email, c, scanner, "451 4.0.0 invalid address") | ||||
| 	} | ||||
| */ | ||||
| 	writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: multipart message nested too deep") | ||||
| } | ||||
| 
 | ||||
| type smtpHandlerFunc func(http.ResponseWriter, *http.Request) | ||||
| 
 | ||||
| func newTestSMTPServer(t *testing.T, handler smtpHandlerFunc) (s *smtp.Server, c net.Conn, conf *Config, scanner *bufio.Scanner) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 binwiederhier
						binwiederhier