From e1d75c0d7fabc7ffb25600941cb0c9b8267c869b Mon Sep 17 00:00:00 2001 From: Melvin Vermeeren Date: Wed, 20 Sep 2023 17:28:53 +0200 Subject: [PATCH] Implement Reply-To header support when replying The Reply-To header, if present, will take precedence over the From header. If the Reply-To data is undefined old behaviour is used. Note that Horde libraries appear to internally determine reply_to property in the Horde_Imap_Client_Data_Envelope (unless Dovecot does it, but doubt). It was observed that reply_to property had the same value as from property in a mail that contained no Reply-To header. It is assumed that the Horde implementation makes sense, so just use the value as-is. This notably fixes replying to mail from software reliant on the Reply-To header such as GitLab and GitHub notifications and many types of mail based ticketing (support) systems. Signed-off-by: Melvin Vermeeren --- lib/Model/IMAPMessage.php | 11 +++++++---- lib/Model/IMessage.php | 10 ++++++++++ lib/Model/Message.php | 20 ++++++++++++++++++++ src/ReplyBuilder.js | 14 +++++++++----- src/store/actions.js | 4 ++-- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php index 1e8d7b8b73..42f25c3708 100644 --- a/lib/Model/IMAPMessage.php +++ b/lib/Model/IMAPMessage.php @@ -528,6 +528,7 @@ public function jsonSerialize() { 'messageId' => $this->getMessageId(), 'from' => $this->getFrom()->jsonSerialize(), 'to' => $this->getTo()->jsonSerialize(), + 'replyTo' => $this->getReplyTo()->jsonSerialize(), 'cc' => $this->getCC()->jsonSerialize(), 'bcc' => $this->getBCC()->jsonSerialize(), 'subject' => $this->getSubject(), @@ -739,17 +740,19 @@ public function setInReplyTo(string $id) { /** * @return AddressList */ - public function getReplyTo() { + public function getReplyTo(): AddressList { return AddressList::fromHorde($this->getEnvelope()->reply_to); } /** - * @param string $id + * @param AddressList $replyTo + * + * @throws Exception * * @return void */ - public function setReplyTo(string $id) { - throw new Exception('not implemented'); + public function setReplyTo(AddressList $replyTo) { + throw new Exception('IMAP message is immutable'); } /** diff --git a/lib/Model/IMessage.php b/lib/Model/IMessage.php index 6ced883caa..fbc099e089 100644 --- a/lib/Model/IMessage.php +++ b/lib/Model/IMessage.php @@ -69,6 +69,16 @@ public function getTo(): AddressList; */ public function setTo(AddressList $to); + /** + * @return AddressList + */ + public function getReplyTo(): AddressList; + + /** + * @param AddressList $replyTo + */ + public function setReplyTo(AddressList $replyTo); + /** * @return AddressList */ diff --git a/lib/Model/Message.php b/lib/Model/Message.php index ba5ca72425..fed4916ac1 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -42,6 +42,9 @@ class Message implements IMessage { /** @var AddressList */ private $to; + /** @var AddressList */ + private $replyTo; + /** @var AddressList */ private $cc; @@ -63,6 +66,7 @@ class Message implements IMessage { public function __construct() { $this->from = new AddressList(); $this->to = new AddressList(); + $this->replyTo = new AddressList(); $this->cc = new AddressList(); $this->bcc = new AddressList(); } @@ -126,6 +130,22 @@ public function setTo(AddressList $to) { $this->to = $to; } + /** + * @return AddressList + */ + public function getReplyTo(): AddressList { + return $this->replyTo; + } + + /** + * @param AddressList $replyTo + * + * @return void + */ + public function setReplyTo(AddressList $replyTo) { + $this->replyTo = $replyTo; + } + /** * @return AddressList */ diff --git a/src/ReplyBuilder.js b/src/ReplyBuilder.js index e5eef98eac..f7913e2b46 100644 --- a/src/ReplyBuilder.js +++ b/src/ReplyBuilder.js @@ -73,11 +73,15 @@ const RecipientType = Object.seal({ Cc: 2, }) -export const buildRecipients = (envelope, ownAddress) => { +export const buildRecipients = (envelope, ownAddress, replyTo) => { let recipientType = RecipientType.None const isOwnAddress = (a) => a.email === ownAddress.email const isNotOwnAddress = negate(isOwnAddress) + // The Reply-To header has higher precedence than the From header. + // This re-uses Horde's handling of the reply_to field directly. + const from = replyTo !== undefined ? replyTo : envelope.from + // Locate why we received this envelope // Can be in 'to', 'cc' or unknown let replyingAddress = envelope.to.find(isOwnAddress) @@ -97,20 +101,20 @@ export const buildRecipients = (envelope, ownAddress) => { if (recipientType === RecipientType.To) { // Send to everyone except yourself, plus the original sender if not ourself to = envelope.to.filter(isNotOwnAddress) - to = to.concat(envelope.from.filter(isNotOwnAddress)) + to = to.concat(from.filter(isNotOwnAddress)) // CC remains the same cc = envelope.cc } else if (recipientType === RecipientType.Cc) { // Send to the same people, plus the sender if not ourself - to = envelope.to.concat(envelope.from.filter(isNotOwnAddress)) + to = envelope.to.concat(from.filter(isNotOwnAddress)) // All CC values are being kept except the replying address cc = envelope.cc.filter(isNotOwnAddress) } else { // Send to the same recipient and the sender (if not ourself) -> answer all to = envelope.to - to = to.concat(envelope.from.filter(isNotOwnAddress)) + to = to.concat(from.filter(isNotOwnAddress)) // Keep CC values cc = envelope.cc @@ -118,7 +122,7 @@ export const buildRecipients = (envelope, ownAddress) => { // edge case: pure self-sent email if (to.length === 0) { - to = envelope.from + to = from } return { diff --git a/src/store/actions.js b/src/store/actions.js index 69f03dbaa9..4751451b1d 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -345,7 +345,7 @@ export default { commit('showMessageComposer', { data: { accountId: reply.data.accountId, - to: reply.data.from, + to: original.replyTo !== undefined ? original.replyTo : reply.data.from, cc: [], subject: buildReplySubject(reply.data.subject), body: data.body, @@ -360,7 +360,7 @@ export default { const recipients = buildReplyRecipients(reply.data, { email: account.emailAddress, label: account.name, - }) + }, original.replyTo) commit('showMessageComposer', { data: { accountId: reply.data.accountId,