diff options
author | KN4CK3R <admin@oldschoolhack.me> | 2023-01-14 16:57:10 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-14 23:57:10 +0800 |
commit | fc037b4b825f0501a1489e10d7c822435d825cb7 (patch) | |
tree | 551590b5ec197d8efca8b7bc3a9acc5961637d9d /services/mailer/token/token.go | |
parent | 20e3ffd2085d7066b3206809dfae7b6ebd59cb5d (diff) | |
download | gitea-fc037b4b825f0501a1489e10d7c822435d825cb7.tar.gz gitea-fc037b4b825f0501a1489e10d7c822435d825cb7.zip |
Add support for incoming emails (#22056)
closes #13585
fixes #9067
fixes #2386
ref #6226
ref #6219
fixes #745
This PR adds support to process incoming emails to perform actions.
Currently I added handling of replies and unsubscribing from
issues/pulls. In contrast to #13585 the IMAP IDLE command is used
instead of polling which results (in my opinion 😉) in cleaner code.
Procedure:
- When sending an issue/pull reply email, a token is generated which is
present in the Reply-To and References header.
- IMAP IDLE waits until a new email arrives
- The token tells which action should be performed
A possible signature and/or reply gets stripped from the content.
I added a new service to the drone pipeline to test the receiving of
incoming mails. If we keep this in, we may test our outgoing emails too
in future.
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Diffstat (limited to 'services/mailer/token/token.go')
-rw-r--r-- | services/mailer/token/token.go | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go new file mode 100644 index 0000000000..8a5a762d6b --- /dev/null +++ b/services/mailer/token/token.go @@ -0,0 +1,128 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package token + +import ( + "context" + crypto_hmac "crypto/hmac" + "crypto/sha256" + "encoding/base32" + "fmt" + "time" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" +) + +// A token is a verifiable container describing an action. +// +// A token has a dynamic length depending on the contained data and has the following structure: +// | Token Version | User ID | HMAC | Payload | +// +// The payload is verifiable by the generated HMAC using the user secret. It contains: +// | Timestamp | Action/Handler Type | Action/Handler Data | + +const ( + tokenVersion1 byte = 1 + tokenLifetimeInYears int = 1 +) + +type HandlerType byte + +const ( + UnknownHandlerType HandlerType = iota + ReplyHandlerType + UnsubscribeHandlerType +) + +var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding) + +type ErrToken struct { + context string +} + +func (err *ErrToken) Error() string { + return "invalid email token: " + err.context +} + +func (err *ErrToken) Unwrap() error { + return util.ErrInvalidArgument +} + +// CreateToken creates a token for the action/user tuple +func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, error) { + payload, err := util.PackData( + time.Now().AddDate(tokenLifetimeInYears, 0, 0).Unix(), + ht, + data, + ) + if err != nil { + return "", err + } + + packagedData, err := util.PackData( + user.ID, + generateHmac([]byte(user.Rands), payload), + payload, + ) + if err != nil { + return "", err + } + + return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil +} + +// ExtractToken extracts the action/user tuple from the token and verifies the content +func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) { + data, err := encodingWithoutPadding.DecodeString(token) + if err != nil { + return UnknownHandlerType, nil, nil, err + } + + if len(data) < 1 { + return UnknownHandlerType, nil, nil, &ErrToken{"no data"} + } + + if data[0] != tokenVersion1 { + return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])} + } + + var userID int64 + var hmac []byte + var payload []byte + if err := util.UnpackData(data[1:], &userID, &hmac, &payload); err != nil { + return UnknownHandlerType, nil, nil, err + } + + user, err := user_model.GetUserByID(ctx, userID) + if err != nil { + return UnknownHandlerType, nil, nil, err + } + + if !crypto_hmac.Equal(hmac, generateHmac([]byte(user.Rands), payload)) { + return UnknownHandlerType, nil, nil, &ErrToken{"verification failed"} + } + + var expiresUnix int64 + var handlerType HandlerType + var innerPayload []byte + if err := util.UnpackData(payload, &expiresUnix, &handlerType, &innerPayload); err != nil { + return UnknownHandlerType, nil, nil, err + } + + if time.Unix(expiresUnix, 0).Before(time.Now()) { + return UnknownHandlerType, nil, nil, &ErrToken{"token expired"} + } + + return handlerType, user, innerPayload, nil +} + +// generateHmac creates a trunkated HMAC for the given payload +func generateHmac(secret, payload []byte) []byte { + mac := crypto_hmac.New(sha256.New, secret) + mac.Write(payload) + hmac := mac.Sum(nil) + + return hmac[:10] // RFC2104 recommends not using less then 80 bits +} |