Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 60 additions & 17 deletions src/ReplyBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,67 @@ const RecipientType = Object.seal({
Cc: 2,
})

export function buildRecipients(envelope, ownAddress, replyTo) {
export function isSameEmailAddress(left, right) {
const normalizedLeft = left?.trim().toLowerCase()
return !!normalizedLeft && normalizedLeft === right?.trim().toLowerCase()
}

export function selectReplyIdentity(envelope, accounts, preferSender = false) {
const identities = accounts
.filter((account) => !account.isUnified && account.connectionStatus !== false)
.flatMap((account) => [
{
accountId: account.id,
aliasId: null,
email: account.emailAddress,
label: account.name,
},
...(account.aliases ?? []).map((alias) => ({
accountId: account.id,
aliasId: alias.id,
email: alias.alias,
label: alias.name,
})),
])
const identityGroups = [
identities.filter((identity) => identity.accountId === envelope.accountId),
identities.filter((identity) => identity.accountId !== envelope.accountId),
]
const addressLists = preferSender
? [envelope.from, envelope.to, envelope.cc]
: [envelope.to, envelope.cc, envelope.from]

for (const addresses of addressLists) {
for (const candidates of identityGroups) {
for (const address of addresses ?? []) {
const identity = candidates.find((candidate) => isSameEmailAddress(candidate.email, address.email))
if (identity) {
return identity
}
}
}
}
}

export function buildRecipients(envelope, ownAddress, replyTo, followUp = false) {
let recipientType = RecipientType.None
const isOwnAddress = (a) => a.email === ownAddress.email
const isOwnAddress = (a) => isSameEmailAddress(a.email, ownAddress.email)
const isNotOwnAddress = negate(isOwnAddress)
const originalTo = envelope.to ?? []
const originalCc = envelope.cc ?? []

// The Reply-To header has higher precedence than the From header.
// This reuses Horde's handling of the reply_to field directly.
const from = replyTo?.length > 0 ? replyTo : envelope.from
const from = !followUp && replyTo?.length > 0 ? replyTo : (envelope.from ?? [])
const senders = followUp ? [] : from.filter(isNotOwnAddress)

// Locate why we received this envelope
// Can be in 'to', 'cc' or unknown
let replyingAddress = envelope.to.find(isOwnAddress)
let replyingAddress = originalTo.find(isOwnAddress)
if (replyingAddress !== undefined) {
recipientType = RecipientType.To
} else {
replyingAddress = envelope.cc.find(isOwnAddress)
replyingAddress = originalCc.find(isOwnAddress)
if (replyingAddress !== undefined) {
recipientType = RecipientType.Cc
} else {
Expand All @@ -82,29 +127,27 @@ export function buildRecipients(envelope, ownAddress, replyTo) {
let cc
if (recipientType === RecipientType.To) {
// Send to everyone except yourself, plus the original sender if not ourself
to = envelope.to.filter(isNotOwnAddress)
to = to.concat(from.filter(isNotOwnAddress))
to = originalTo.filter(isNotOwnAddress)
to = to.concat(senders)

// CC remains the same
cc = envelope.cc
cc = originalCc.filter(isNotOwnAddress)
} else if (recipientType === RecipientType.Cc) {
// Send to the same people, plus the sender if not ourself
to = envelope.to.concat(from.filter(isNotOwnAddress))
to = originalTo.concat(senders)

// All CC values are being kept except the replying address
cc = envelope.cc.filter(isNotOwnAddress)
cc = originalCc.filter(isNotOwnAddress)
} else {
// Send to the same recipient and the sender (if not ourself) -> answer all
to = envelope.to
to = to.concat(from.filter(isNotOwnAddress))
to = originalTo
to = to.concat(senders)

// Keep CC values
cc = envelope.cc
cc = originalCc.filter(isNotOwnAddress)
}

// edge case: pure self-sent email
if (to.length === 0) {
to = from
if (to.length === 0 && cc.length === 0) {
to = followUp ? (originalTo.length > 0 ? originalTo : originalCc) : from
}

return {
Expand Down
4 changes: 1 addition & 3 deletions src/components/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1478,9 +1478,7 @@ export default {
this.changeSignature = true

this.$emit('update:fromAccount', alias.id)
if (alias.aliasId) {
this.$emit('update:fromAlias', alias.aliasId)
}
this.$emit('update:fromAlias', alias.aliasId)

if (this.wantsSmimeSign || this.wantsSmimeEncrypt) {
if (!this.smimeCertificateForAlias(alias)) {
Expand Down
6 changes: 2 additions & 4 deletions src/components/ThreadEnvelope.vue
Original file line number Diff line number Diff line change
Expand Up @@ -629,10 +629,8 @@ export default {
accountId: this.envelope.accountId,
})
}
const recipients = buildReplyRecipients(this.envelope, {
label: this.account.name,
email: this.account.emailAddress,
})
const { identity, followUp } = this.mainStore.getReplyContext(this.envelope, { followUp: this.showFollowUpHeader })
const recipients = buildReplyRecipients(this.envelope, identity, undefined, followUp)
return recipients.to.concat(recipients.cc).length > 1
},

Expand Down
55 changes: 42 additions & 13 deletions src/store/mainStore/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {
buildForwardSubject,
buildRecipients as buildReplyRecipients,
buildReplySubject,
isSameEmailAddress,
selectReplyIdentity,
} from '../../ReplyBuilder.js'
import {
create as createAccount,
Expand Down Expand Up @@ -461,6 +463,22 @@ export default function mainStoreActions() {
})
})
},
getReplyContext(envelope, { followUp = false } = {}) {
const account = this.getAccount(envelope.accountId)
const fallback = {
accountId: account.id,
aliasId: null,
email: account.emailAddress,
label: account.name,
}
const mailbox = this.getMailbox(envelope.mailboxId)
const preferSender = followUp || mailbox?.specialRole === 'sent'
|| (envelope.mailboxId !== undefined && envelope.mailboxId === account.sentMailboxId)
const identity = selectReplyIdentity(envelope, this.getAccounts, preferSender) ?? fallback
const isOwnMessage = envelope.from?.length > 0
&& envelope.from.every((address) => isSameEmailAddress(address.email, identity.email))
return { identity, followUp: preferSender || isOwnMessage }
},
async startComposerSession({
type = 'imap',
data = {},
Expand Down Expand Up @@ -520,28 +538,37 @@ export default function mainStoreActions() {
}
}

let replyIdentity
let isFollowUp = false
if (reply.mode === 'reply' || reply.mode === 'replyAll') {
const context = this.getReplyContext(reply.data, { followUp: reply.followUp })
replyIdentity = context.identity
isFollowUp = context.followUp
}

if (reply.mode === 'reply') {
logger.debug('Show simple reply composer', { reply })
const account = this.getAccount(reply.data.accountId)
// For mailing list emails, "Reply to sender" must use From because
// Reply-To points to the list address, not the original sender.
// For regular emails, honor Reply-To if the sender set one.
const isMailingList = !!(original.unsubscribeUrl || original.unsubscribeMailto)
let to = (!isMailingList && original.replyTo?.length > 0)
? original.replyTo
: reply.data.from
let cc = []
// Replying to a message we sent ourselves: follow up with the
// original recipient(s) instead of addressing ourselves.
const isOwnMessage = to.length > 0
&& to.every((addr) => addr.email === account.emailAddress)
if (reply.followUp || isOwnMessage) {
to = reply.data.to
if (isFollowUp) {
const recipients = buildReplyRecipients(reply.data, replyIdentity, undefined, true)
to = recipients.to
cc = recipients.cc
}
this.startComposerSessionMutation({
data: {
accountId: reply.data.accountId,
accountId: replyIdentity.accountId,
aliasId: replyIdentity.aliasId,
to,
cc: [],
cc,
subject: buildReplySubject(reply.data.subject),
isHtml: data.isHtml,
bodyHtml: data.bodyHtml,
Expand All @@ -554,14 +581,16 @@ export default function mainStoreActions() {
return
} else if (reply.mode === 'replyAll') {
logger.debug('Show reply all reply composer', { reply })
const account = this.getAccount(reply.data.accountId)
const recipients = buildReplyRecipients(reply.data, {
email: account.emailAddress,
label: account.name,
}, original.replyTo)
const recipients = buildReplyRecipients(
reply.data,
replyIdentity,
original.replyTo,
isFollowUp,
)
this.startComposerSessionMutation({
data: {
accountId: reply.data.accountId,
accountId: replyIdentity.accountId,
aliasId: replyIdentity.aliasId,
to: recipients.to,
cc: recipients.cc,
subject: buildReplySubject(reply.data.subject),
Expand Down
Loading
Loading