mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-12 13:22:55 +02:00
parent
b28c4f2b08
commit
65f3feaa84
@ -40,7 +40,7 @@ func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content
|
|||||||
if !email.IsActivated {
|
if !email.IsActivated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if content == email.Email {
|
if strings.EqualFold(content, email.LowerEmail) {
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,11 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const IncomingEmailTokenPlaceholder = "%{token}"
|
||||||
|
|
||||||
var IncomingEmail = struct {
|
var IncomingEmail = struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
ReplyToAddress string
|
ReplyToAddress string
|
||||||
TokenPlaceholder string `ini:"-"`
|
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
UseTLS bool `ini:"USE_TLS"`
|
UseTLS bool `ini:"USE_TLS"`
|
||||||
@ -28,7 +29,6 @@ var IncomingEmail = struct {
|
|||||||
}{
|
}{
|
||||||
Mailbox: "INBOX",
|
Mailbox: "INBOX",
|
||||||
DeleteHandledMessage: true,
|
DeleteHandledMessage: true,
|
||||||
TokenPlaceholder: "%{token}",
|
|
||||||
MaximumMessageSize: 10485760,
|
MaximumMessageSize: 10485760,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,19 +54,10 @@ func checkReplyToAddress() error {
|
|||||||
return errors.New("name must not be set")
|
return errors.New("name must not be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
|
placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder)
|
||||||
switch c {
|
userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@")
|
||||||
case 0:
|
if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) {
|
||||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder)
|
||||||
case 1:
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
|
|
||||||
if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
|
|
||||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
net_mail "net/mail"
|
net_mail "net/mail"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -24,31 +23,10 @@ import (
|
|||||||
"github.com/jhillyerd/enmime/v2"
|
"github.com/jhillyerd/enmime/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
addressTokenRegex *regexp.Regexp
|
|
||||||
referenceTokenRegex *regexp.Regexp
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context) error {
|
||||||
if !setting.IncomingEmail.Enabled {
|
if !setting.IncomingEmail.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
addressTokenRegex, err = regexp.Compile(
|
|
||||||
fmt.Sprintf(
|
|
||||||
`\A%s\z`,
|
|
||||||
strings.Replace(regexp.QuoteMeta(setting.IncomingEmail.ReplyToAddress), regexp.QuoteMeta(setting.IncomingEmail.TokenPlaceholder), "(.+)", 1),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
referenceTokenRegex, err = regexp.Compile(fmt.Sprintf(`\Areply-(.+)@%s\z`, regexp.QuoteMeta(setting.Domain)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Incoming Email", process.SystemProcessType, true)
|
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Incoming Email", process.SystemProcessType, true)
|
||||||
defer finished()
|
defer finished()
|
||||||
@ -241,7 +219,7 @@ loop:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerType, user, payload, err := token.ExtractToken(ctx, t)
|
handlerType, user, payload, err := token.DecodeToken(ctx, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*token.ErrToken); ok {
|
if _, ok := err.(*token.ErrToken); ok {
|
||||||
log.Info("Invalid incoming email token: %v", err)
|
log.Info("Invalid incoming email token: %v", err)
|
||||||
@ -292,22 +270,31 @@ func isAutomaticReply(env *enmime.Envelope) bool {
|
|||||||
return autoRespond != ""
|
return autoRespond != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractToken(s, tokenPrefix, tokenSuffix string) string {
|
||||||
|
if len(s) <= len(tokenPrefix)+len(tokenSuffix) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
prefix, suffix := s[0:len(tokenPrefix)], s[len(s)-len(tokenSuffix):]
|
||||||
|
if strings.EqualFold(prefix, tokenPrefix) && strings.EqualFold(suffix, tokenSuffix) {
|
||||||
|
return s[len(tokenPrefix) : len(s)-len(tokenSuffix)]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// searchTokenInHeaders looks for the token in To, Delivered-To and References
|
// searchTokenInHeaders looks for the token in To, Delivered-To and References
|
||||||
func searchTokenInHeaders(env *enmime.Envelope) string {
|
func searchTokenInHeaders(env *enmime.Envelope) string {
|
||||||
if addressTokenRegex != nil {
|
to, _ := env.AddressList("To")
|
||||||
to, _ := env.AddressList("To")
|
|
||||||
|
|
||||||
token := searchTokenInAddresses(to)
|
token := searchTokenInAddresses(to)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
deliveredTo, _ := env.AddressList("Delivered-To")
|
deliveredTo, _ := env.AddressList("Delivered-To")
|
||||||
|
|
||||||
token = searchTokenInAddresses(deliveredTo)
|
token = searchTokenInAddresses(deliveredTo)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
return token
|
return token
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
references := env.GetHeader("References")
|
references := env.GetHeader("References")
|
||||||
@ -322,10 +309,9 @@ func searchTokenInHeaders(env *enmime.Envelope) string {
|
|||||||
if end == -1 || begin > end {
|
if end == -1 || begin > end {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
t := extractToken(references[begin:end], "reply-", "@"+setting.Domain)
|
||||||
match := referenceTokenRegex.FindStringSubmatch(references[begin:end])
|
if t != "" {
|
||||||
if len(match) == 2 {
|
return t
|
||||||
return match[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
references = references[end+1:]
|
references = references[end+1:]
|
||||||
@ -336,15 +322,15 @@ func searchTokenInHeaders(env *enmime.Envelope) string {
|
|||||||
|
|
||||||
// searchTokenInAddresses looks for the token in an address
|
// searchTokenInAddresses looks for the token in an address
|
||||||
func searchTokenInAddresses(addresses []*net_mail.Address) string {
|
func searchTokenInAddresses(addresses []*net_mail.Address) string {
|
||||||
for _, address := range addresses {
|
tokenPrefix, tokenSuffix, _ := strings.Cut(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder)
|
||||||
match := addressTokenRegex.FindStringSubmatch(address.Address)
|
if tokenSuffix == "" {
|
||||||
if len(match) != 2 {
|
return ""
|
||||||
continue
|
}
|
||||||
}
|
for _, address := range addresses {
|
||||||
|
if t := extractToken(address.Address, tokenPrefix, tokenSuffix); t != "" {
|
||||||
return match[1]
|
return t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/jhillyerd/enmime/v2"
|
"github.com/jhillyerd/enmime/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -68,6 +70,18 @@ func TestIsAutomaticReply(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchTokenInHeadersCaseInsensitive(t *testing.T) {
|
||||||
|
setting.IncomingEmail.ReplyToAddress = "InComing+%{token}@ExAmPle.com"
|
||||||
|
setting.Domain = "DoMain.com"
|
||||||
|
mkEnv := func(s string) *enmime.Envelope {
|
||||||
|
env, _ := enmime.ReadEnvelope(strings.NewReader(s + "\r\n\r\n"))
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("To: incoming+abc@EXAMPLE.COM")))
|
||||||
|
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("Delivered-To: INCOMING+abc@example.com")))
|
||||||
|
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("References: <ReplY-abc@DomaiN.COM>")))
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetContentFromMailReader(t *testing.T) {
|
func TestGetContentFromMailReader(t *testing.T) {
|
||||||
mailString := "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
|
mailString := "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
|
|||||||
@ -182,7 +182,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateToken failed: %v", err)
|
log.Error("CreateToken failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
replyAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
|
replyAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1)
|
||||||
msg.ReplyTo = replyAddress
|
msg.ReplyTo = replyAddress
|
||||||
msg.SetHeader("List-Post", fmt.Sprintf("<mailto:%s>", replyAddress))
|
msg.SetHeader("List-Post", fmt.Sprintf("<mailto:%s>", replyAddress))
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateToken failed: %v", err)
|
log.Error("CreateToken failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
unsubAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
|
unsubAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1)
|
||||||
listUnsubscribe = append(listUnsubscribe, "<mailto:"+unsubAddress+">")
|
listUnsubscribe = append(listUnsubscribe, "<mailto:"+unsubAddress+">")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@ -73,9 +74,11 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er
|
|||||||
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
|
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractToken extracts the action/user tuple from the token and verifies the content
|
// DecodeToken decodes the handler, user and payload from the token and verifies the content
|
||||||
func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
|
func DecodeToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
|
||||||
data, err := encodingWithoutPadding.DecodeString(token)
|
// MTAs are permitted to alter the case of the local-part (RFC 5321 §2.4), so normalize
|
||||||
|
// to the base32 alphabet before decoding to survive a lowercased reply-to address.
|
||||||
|
data, err := encodingWithoutPadding.DecodeString(strings.ToUpper(token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UnknownHandlerType, nil, nil, err
|
return UnknownHandlerType, nil, nil, err
|
||||||
}
|
}
|
||||||
@ -118,11 +121,11 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
|
|||||||
return handlerType, user, innerPayload, nil
|
return handlerType, user, innerPayload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateHmac creates a trunkated HMAC for the given payload
|
// generateHmac creates a truncated HMAC for the given payload
|
||||||
func generateHmac(secret, payload []byte) []byte {
|
func generateHmac(secret, payload []byte) []byte {
|
||||||
mac := crypto_hmac.New(sha256.New, secret)
|
mac := crypto_hmac.New(sha256.New, secret)
|
||||||
mac.Write(payload)
|
mac.Write(payload)
|
||||||
hmac := mac.Sum(nil)
|
hmac := mac.Sum(nil)
|
||||||
|
|
||||||
return hmac[:10] // RFC2104 recommends not using less then 80 bits
|
return hmac[:10] // RFC2104 recommends not using less than 80 bits
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,14 @@ func TestIncomingEmail(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotEmpty(t, token)
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
ht, u, p, err := token_service.ExtractToken(t.Context(), token)
|
ht, u, p, err := token_service.DecodeToken(t.Context(), token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, token_service.ReplyHandlerType, ht)
|
||||||
|
assert.Equal(t, user.ID, u.ID)
|
||||||
|
assert.Equal(t, payload, p)
|
||||||
|
|
||||||
|
// MTAs may lowercase the local-part of the reply-to address (RFC 5321 §2.4).
|
||||||
|
ht, u, p, err = token_service.DecodeToken(t.Context(), strings.ToLower(token))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, token_service.ReplyHandlerType, ht)
|
assert.Equal(t, token_service.ReplyHandlerType, ht)
|
||||||
assert.Equal(t, user.ID, u.ID)
|
assert.Equal(t, user.ID, u.ID)
|
||||||
@ -189,7 +196,7 @@ func TestIncomingEmail(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
msg := sender_service.NewMessageFrom(
|
msg := sender_service.NewMessageFrom(
|
||||||
strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1),
|
strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1),
|
||||||
"",
|
"",
|
||||||
user.Email,
|
user.Email,
|
||||||
"",
|
"",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user