mirror of
				https://github.com/binwiederhier/ntfy.git
				synced 2025-10-30 20:42:09 +01:00 
			
		
		
		
	WIP
This commit is contained in:
		
							parent
							
								
									a75fb08ef1
								
							
						
					
					
						commit
						d88dbbc90f
					
				
					 4 changed files with 200 additions and 11 deletions
				
			
		
							
								
								
									
										3
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -27,6 +27,7 @@ require github.com/pkg/errors v0.9.1 // indirect | |||
| 
 | ||||
| require ( | ||||
| 	firebase.google.com/go/v4 v4.10.0 | ||||
| 	github.com/microcosm-cc/bluemonday v1.0.23 | ||||
| 	github.com/prometheus/client_golang v1.14.0 | ||||
| 	github.com/stripe/stripe-go/v74 v74.14.0 | ||||
| ) | ||||
|  | @ -39,6 +40,7 @@ require ( | |||
| 	cloud.google.com/go/longrunning v0.4.1 // indirect | ||||
| 	github.com/AlekSi/pointer v1.2.0 // indirect | ||||
| 	github.com/MicahParks/keyfunc v1.9.0 // indirect | ||||
| 	github.com/aymerick/douceur v0.2.0 // indirect | ||||
| 	github.com/beorn7/perks v1.0.1 // indirect | ||||
| 	github.com/cespare/xxhash/v2 v2.2.0 // indirect | ||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||
|  | @ -50,6 +52,7 @@ require ( | |||
| 	github.com/google/uuid v1.3.0 // indirect | ||||
| 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect | ||||
| 	github.com/googleapis/gax-go/v2 v2.8.0 // indirect | ||||
| 	github.com/gorilla/css v1.0.0 // indirect | ||||
| 	github.com/kr/pretty v0.3.1 // indirect | ||||
| 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||
|  |  | |||
							
								
								
									
										6
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -22,6 +22,8 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak | |||
| github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | ||||
| github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= | ||||
| github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= | ||||
| github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= | ||||
| github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= | ||||
| github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= | ||||
| github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= | ||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||
|  | @ -85,6 +87,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9 | |||
| github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= | ||||
| github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc= | ||||
| github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= | ||||
| github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= | ||||
| github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= | ||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||
| github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||||
|  | @ -95,6 +99,8 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp | |||
| github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= | ||||
| github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= | ||||
| github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= | ||||
| github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= | ||||
| github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= | ||||
| github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI= | ||||
| github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= | ||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import ( | |||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/emersion/go-smtp" | ||||
| 	"github.com/microcosm-cc/bluemonday" | ||||
| 	"io" | ||||
| 	"mime" | ||||
| 	"mime/multipart" | ||||
|  | @ -13,6 +14,7 @@ import ( | |||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/mail" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| ) | ||||
|  | @ -231,37 +233,66 @@ func readMailBody(body io.Reader, header mail.Header) (string, error) { | |||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if strings.ToLower(contentType) == "text/plain" { | ||||
| 		return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding")) | ||||
| 	} else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") { | ||||
| 		return readMultipartMailBody(body, params, 0) | ||||
| 	canonicalContentType := strings.ToLower(contentType) | ||||
| 	if canonicalContentType == "text/plain" || canonicalContentType == "text/html" { | ||||
| 		return readTextMailBody(body, canonicalContentType, header.Get("Content-Transfer-Encoding")) | ||||
| 	} else if strings.HasPrefix(canonicalContentType, "multipart/") { | ||||
| 		return readMultipartMailBody(body, params) | ||||
| 	} | ||||
| 	return "", errUnsupportedContentType | ||||
| } | ||||
| 
 | ||||
| func readMultipartMailBody(body io.Reader, params map[string]string, depth int) (string, error) { | ||||
| func readMultipartMailBody(body io.Reader, params map[string]string) (string, error) { | ||||
| 	parts := make(map[string]string) | ||||
| 	if err := readMultipartMailBodyParts(body, params, 0, parts); err != nil && err != io.EOF { | ||||
| 		return "", err | ||||
| 	} else if s, ok := parts["text/plain"]; ok { | ||||
| 		return s, nil | ||||
| 	} else if s, ok := parts["text/html"]; ok { | ||||
| 		return s, nil | ||||
| 	} | ||||
| 	return "", io.EOF | ||||
| } | ||||
| 
 | ||||
| func readMultipartMailBodyParts(body io.Reader, params map[string]string, depth int, parts map[string]string) error { | ||||
| 	if depth >= maxMultipartDepth { | ||||
| 		return "", errMultipartNestedTooDeep | ||||
| 		return errMultipartNestedTooDeep | ||||
| 	} | ||||
| 	mr := multipart.NewReader(body, params["boundary"]) | ||||
| 	for { | ||||
| 		part, err := mr.NextPart() | ||||
| 		if err != nil { // may be io.EOF | ||||
| 			return "", err | ||||
| 			return err | ||||
| 		} | ||||
| 		partContentType, partParams, err := mime.ParseMediaType(part.Header.Get("Content-Type")) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 			return err | ||||
| 		} | ||||
| 		if strings.ToLower(partContentType) == "text/plain" { | ||||
| 			return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding")) | ||||
| 		canonicalPartContentType := strings.ToLower(partContentType) | ||||
| 		if canonicalPartContentType == "text/plain" || canonicalPartContentType == "text/html" { | ||||
| 			s, err := readTextMailBody(part, canonicalPartContentType, part.Header.Get("Content-Transfer-Encoding")) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			parts[canonicalPartContentType] = s | ||||
| 		} else if strings.HasPrefix(strings.ToLower(partContentType), "multipart/") { | ||||
| 			return readMultipartMailBody(part, partParams, depth+1) | ||||
| 			if err := readMultipartMailBodyParts(part, partParams, depth+1, parts); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 		// Continue with next part | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func readTextMailBody(reader io.Reader, contentType, transferEncoding string) (string, error) { | ||||
| 	if contentType == "text/plain" { | ||||
| 		return readPlainTextMailBody(reader, transferEncoding) | ||||
| 	} else if contentType == "text/html" { | ||||
| 		return readHTMLMailBody(reader, transferEncoding) | ||||
| 	} | ||||
| 	return "", fmt.Errorf("unsupported content type: %s", contentType) | ||||
| } | ||||
| 
 | ||||
| func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, error) { | ||||
| 	if strings.ToLower(transferEncoding) == "base64" { | ||||
| 		reader = base64.NewDecoder(base64.StdEncoding, reader) | ||||
|  | @ -272,3 +303,27 @@ func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, e | |||
| 	} | ||||
| 	return string(body), nil | ||||
| } | ||||
| 
 | ||||
| func readHTMLMailBody(reader io.Reader, transferEncoding string) (string, error) { | ||||
| 	body, err := readPlainTextMailBody(reader, transferEncoding) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	stripped := bluemonday. | ||||
| 		StrictPolicy(). | ||||
| 		AddSpaceWhenStrippingTag(true). | ||||
| 		Sanitize(body) | ||||
| 	return removeExtraEmptyLines(stripped), nil | ||||
| } | ||||
| 
 | ||||
| func removeExtraEmptyLines(str string) string { | ||||
| 	// Replace lines that contain only spaces with empty lines | ||||
| 	re := regexp.MustCompile(`(?m)^\s+$`) | ||||
| 	str = re.ReplaceAllString(str, "") | ||||
| 
 | ||||
| 	// Remove more than 2 consecutive empty lines | ||||
| 	re = regexp.MustCompile(`\n{3,}`) | ||||
| 	str = re.ReplaceAllString(str, "\n\n") | ||||
| 
 | ||||
| 	return str | ||||
| } | ||||
|  |  | |||
|  | @ -492,6 +492,131 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== | |||
| 	writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: multipart message nested too deep") | ||||
| } | ||||
| 
 | ||||
| func TestSmtpBackend_HTMLEmail(t *testing.T) { | ||||
| 	email := `EHLO example.com | ||||
| MAIL FROM: test@mydomain.me | ||||
| RCPT TO: ntfy-mytopic@ntfy.sh | ||||
| DATA | ||||
| Message-Id: <51610934ss4.mmailer@fritz.box> | ||||
| From: <email@email.com> | ||||
| To: <email@email.com>, | ||||
| 	<ntfy-subjectatntfy@ntfy.sh> | ||||
| Date: Thu, 30 Mar 2023 02:56:53 +0000 | ||||
| Subject: A HTML email | ||||
| Mime-Version: 1.0 | ||||
| Content-Type: text/html; | ||||
| 	charset="utf-8" | ||||
| Content-Transfer-Encoding: quoted-printable | ||||
| 
 | ||||
| <=21DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
| <title>Alerttitle</title> | ||||
| <meta http-equiv=3D"content-type" content=3D"text/html;charset=3Dutf-8"/> | ||||
| </head> | ||||
| <body style=3D"color: =23000000; background-color: =23f0eee6;"> | ||||
| <table width=3D"100%" align=3D"center" style=3D"border:solid 2px =23eeeeee= | ||||
| ; border-collapse: collapse;"> | ||||
| <tr> | ||||
| <td> | ||||
| <table style=3D"border-collapse: collapse;"> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| <tr> | ||||
| <td style=3D"background: =23FFFFFF;"> | ||||
| <table style=3D"color: =23FFFFFF; background-color: =23006EC0; border-coll= | ||||
| apse: collapse;"> | ||||
| <tr> | ||||
| <td style=3D"width: 1000px; text-align: center; font-size: 18pt; font-fami= | ||||
| ly: Arial, Helvetica, sans-serif; padding: 10px;"> | ||||
| 
 | ||||
| 
 | ||||
| headertext of table | ||||
| 
 | ||||
| </td> | ||||
| </tr> | ||||
| </table> | ||||
| </td> | ||||
| </tr> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| <tr> | ||||
| <td style=3D"padding: 10px 20px; background: =23FFFFFF;"> | ||||
| <table style=3D"border-collapse: collapse;"> | ||||
| <tr> | ||||
| <td style=3D"width: 940px; font-size: 13pt; font-family: Arial, Helvetica,= | ||||
|  sans-serif; text-align: left;"> | ||||
| " Very important information about a change in your | ||||
| home automation setup  | ||||
| 
 | ||||
| Now the light is on | ||||
| </td> | ||||
| </tr> | ||||
| </table> | ||||
| </td> | ||||
| </tr> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| <tr> | ||||
| <td style=3D"padding: 10px 20px; background: =23FFFFFF;"> | ||||
| <table> | ||||
| <tr> | ||||
| <td style=3D"width: 960px; font-size: 10pt; font-family: Arial, Helvetica,= | ||||
|  sans-serif; text-align: left;"> | ||||
| <hr /> | ||||
| If you don't want to recieve this message anymore, stop the push | ||||
|  services in your <a href=3D"https:fritzbox" target=3D"_= | ||||
| blank">FRITZ=21Box</a>=2E<br /> | ||||
| Here you can see the active push services: "System > Push Service"=2E | ||||
| </td> | ||||
| </tr> | ||||
| </table> | ||||
| </td> | ||||
| </tr> | ||||
| <tr> | ||||
| <td> | ||||
| <table style=3D"color: =23FFFFFF; background-color: =23006EC0;"> | ||||
| <tr> | ||||
| <td style=3D"width: 1000px; font-size: 10pt; font-family: Arial, Helvetica= | ||||
| , sans-serif; text-align: center; padding: 10px;"> | ||||
| This mail has ben sent by your <a style=3D"color: =23FFFFFF;" href=3D"https:= | ||||
| //fritzbox" target=3D"_blank">FRITZ=21Box</a= | ||||
| > automatically=2E | ||||
| </td> | ||||
| </tr> | ||||
| </table> | ||||
| </td> | ||||
| </tr> | ||||
| </table> | ||||
| </td> | ||||
| </tr> | ||||
| </table> | ||||
| </body> | ||||
| </html> | ||||
| . | ||||
| ` | ||||
| 
 | ||||
| 	s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) { | ||||
| 		require.Equal(t, "/mytopic", r.URL.Path) | ||||
| 		require.Equal(t, "A HTML email", r.Header.Get("Title")) | ||||
| 		require.Equal(t, "what's up", readAll(t, r.Body)) | ||||
| 	}) | ||||
| 	defer s.Close() | ||||
| 	defer c.Close() | ||||
| 	writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") | ||||
| } | ||||
| 
 | ||||
| func TestSmtpBackend_PlaintextWithToken(t *testing.T) { | ||||
| 	email := `EHLO example.com | ||||
| MAIL FROM: phil@example.com | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 binwiederhier
						binwiederhier