Description
As far as I can tell, gitea has no support for receiving email responses from users. When a user receives a message, their only option for response is to open the web interface and use a web browser. This is rarely convenient for discussions while mobile, especially if your git services passwords are >200 chars. I believe this is a critical feature that needs discussion and implementation.
From my perspective, the big picture pieces are:
1a. fetch messages
1b. receive messages
2. validate messages (including duplicate check)
3. append information to the ticket
(Note: ticket -> issue or pull)
1a.
- I think IMAP is the standard. Gitea is already has source that interacts with IMAP/S services. It could just be a matter of marking a message as read, pulling it down, and dropping it into a processing queue. Of course, offering a DELETE_MESSAGE=(true|false) will always be appreciated.
1b.
- Listening for an SMTP connection sounds neat, but please don't ever consider it. This would be a pretty scary attack surface for quite a number of reasons. (I typed out a lot here, but... tl;dr-- postfix kicks butt at being a mail server, gitea should not reinvent that wheel). Instead, I'd like to suggest listening on a unix socket.
In settings where this would be observed, you can expect to see custom-written middleware that does things like check the spamassassin score. This middleware then opens a socket to write the message content to.
In the case of 1b
, it's almost exactly like 1a
, except that it listens to a socket for messages instead of connecting to an outside service, trying to handle duplicate message race conditions, etc. The former (1a) would always work well for single-node setups where the latter (1b) would be available for handling scale.
2.
- I envision another process pulling messages off of a queue (another redis db sounds nice) and doing some validation before letting the message be included in a ticket.
I'd like to start with opinions (stated as expectations):
- A user may only respond to messages; always reply, never compose.
- A user may only respond to the ticket the message was sent from,
- Any computational verification should be minimally invasive (not taxing).
- Verification should be nearly stateless; avoid storing large tables of tokens.
- A user may not use their response to impersonate another user. (@strk: fun times!)
- User-visible tokens must be cryptographically secure.
Now, for the fun part... I've been poking around at how github handles this and I think would could handle a simple implementation that meets (my) expectations. This would require adding a "salt" column to the user table.
I have a larger vision of an overall workflow, but starting the the explanation at this point is easiest. At 2.
we will have a From: address that looks similar to github's. This is in the form "reply+<glob>@reply.domain.tld", where <glob> is a series of hashed and non-hashed values. We will also have an In-Reply-To: field that tells us where this message should be directed.
With <glob> taken out, we can dissect it into it's various components (my thoughts):
- $user-id => Our copy of the user-id, hex value, left zero-padded to 10 digits
- $key => A cryptographically pseudo-random (i.e. cat /dev/urandom | md5sum) value the user gets
- $cksum => A value we generated with a combination of the $key + <the user's current salt>
From the Message-ID field, we will have these components available:
- $locator => <team|user>/<project>/<issue|pull>/<number>
- $replied-to => The specific message being replied to. (or just the timestamp of it)
[ note: ^ the sources of these values are what I skipped earlier .. coming up soon ]
To verify this message is a reply from a message we sent to that user (email.. considered good enough for password resets), we'll be able to run the formula:
user_hash = checksum($user-id + $key + _user_salt($user-id) + $locator + $replied-to)
if $user_hash != $cksum { drop_message() }
elif get_post_date($replied-to) >= 90days { drop_message() }
elif # could possibly also check From: and verify it came from an address on file (maybe...)
else { post_message($locator, $user-id, $message-body) }
That works because... When we sent the message, we set two fields: Reply-To
and Message-ID.
Ex: Message-ID: <go-gitea/gitea/issue/2351/1220588887@domain.tld>
; the exact structure could be different, but I think this is fairly straight-forward (note: the produced message-id [https://en.wikipedia.org/wiki/Message-ID])must always) be globally unique--except when sending the same message to multiple recipents). This is the message the user is replying to and it's post-date gets to be used in our TTL/validity calculation. The mail client should turn this into In-Reply-To
for us to verify later.
Ex: Reply-To: reply+<glob>@domain.tld
; This is where we'll want the message routed. We should be able to customize the exact "reply" user. This allows formats such as mycorp-inbound+<glob>@gmail.com
without causing routing problems because this is a format that gmail permits.
The <glob> portion was explained earlier. It takes the form: $user-id$key$cksum
.
2.
summary:
In the end, all this does is generate a random value, combine it with a user salt and some other things, and then append some extra information to the email.
When we get the message back we verify that we can re-produce the $cksum they provided. Because they've never had access to the salt, this is a value they can't produce on their own. Provided the key is sufficiently large, I believe this is more than adequate to provide this feature without opening the doors for abuse. This allows us to automatically invalidate old tokens with one simple check (is $message-id older than X days?).
= Discussion!
I know this is massive, and probably not without mistakes, but I'd prefer get the discussion started since I can't see one person implementing this by themselves.