diff --git a/lib/Address.php b/lib/Address.php index 3dc44c0c17..69ac3773d9 100644 --- a/lib/Address.php +++ b/lib/Address.php @@ -34,7 +34,7 @@ public static function fromHorde(Horde_Mail_Rfc822_Address $horde): self { } public static function fromRaw(string $label, string $email): self { - $wrapped = new Horde_Mail_Rfc822_Address($email); + $wrapped = new Horde_Mail_Rfc822_Address(strtolower($email)); // If no label is set we use the email if ($label !== $email) { $wrapped->personal = $label; diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 4276814034..885c48a44a 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -145,6 +145,12 @@ public function getForm() { $this->classificationSettingsService->isClassificationEnabledByDefault(), ); + $this->initialStateService->provideInitialState( + Application::APP_ID, + 'digikala_domain', + $this->config->getAppValue('mail', 'digikala_domain', '@digikala.com'), + ); + return new TemplateResponse(Application::APP_ID, 'settings-admin'); } diff --git a/src/components/Composer.vue b/src/components/Composer.vue index 00d6134ce8..c876c73437 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -1295,7 +1295,7 @@ export default { || account.name.toLowerCase().indexOf(term.toLowerCase()) !== -1, ) .map(account => ({ - email: account.emailAddress, + email: account.emailAddress.toLowerCase(), label: account.name, })) this.autocompleteRecipients = uniqBy('email')(this.autocompleteRecipients.concat(selfRecipients)) @@ -1353,7 +1353,7 @@ export default { return } option = {} - option.email = this.recipientSearchTerms[type] + option.email = this.recipientSearchTerms[type].toLowerCase() option.label = this.recipientSearchTerms[type] this.recipientSearchTerms[type] = '' } @@ -1523,7 +1523,7 @@ export default { if (!this.seemsValidEmailAddress(value)) { throw new Error('Skipping because it does not look like a valid email address') } - return { email: value, label: value } + return { email: value.toLowerCase(), label: value } }, /** diff --git a/src/components/Imip.vue b/src/components/Imip.vue index e9091c600e..4a40351606 100644 --- a/src/components/Imip.vue +++ b/src/components/Imip.vue @@ -123,6 +123,7 @@ import { flatten } from 'ramda' import { showError } from '@nextcloud/dialogs' import useMainStore from '../store/mainStore.js' import { mapState } from 'pinia' +import { loadState } from '@nextcloud/initial-state' // iMIP methods const REQUEST = 'REQUEST' @@ -157,6 +158,22 @@ function findAttendee(vEvent, email) { return undefined } +function findAttendeeByEmails(vEvent, emails) { + if (!vEvent || !Array.isArray(emails) || emails.length === 0) { + return undefined + } + + const emailSet = new Set(emails.map(e => removeMailtoPrefix(e).toLowerCase())) + for (const attendee of [...vEvent.getPropertyIterator('ORGANIZER'), ...vEvent.getAttendeeIterator()]) { + const normalized = removeMailtoPrefix(attendee.email).toLowerCase() + if (emailSet.has(normalized)) { + return attendee + } + } + + return undefined +} + export default { name: 'Imip', components: { @@ -173,6 +190,10 @@ export default { type: Object, required: true, }, + message: { + type: Object, + required: true, + }, }, data() { return { @@ -190,6 +211,7 @@ export default { existingEventFetched: false, targetCalendar: undefined, comment: '', + digikalaDomain: loadState('mail', 'digikala_domain', '@digikala.com'), } }, computed: { @@ -197,6 +219,7 @@ export default { currentUserPrincipalEmail: 'getCurrentUserPrincipalEmail', clonedWriteableCalendars: 'getClonedWriteableCalendars', currentUserPrincipal: 'getCurrentUserPrincipal', + accounts: 'getAccounts', }), /** @@ -208,6 +231,14 @@ export default { return this.scheduling.method }, + isFromDigikala() { + if (!this.message.from || !this.message.from[0]) { + return false + } + const fromEmail = this.message.from[0].email?.toLowerCase() || '' + return fromEmail.endsWith(this.digikalaDomain) + }, + /** * @return {boolean} */ @@ -305,7 +336,7 @@ export default { * @return {boolean} */ userIsAttendee() { - return !!findAttendee(this.attachedVEvent, this.currentUserPrincipalEmail) + return !!findAttendeeByEmails(this.attachedVEvent, this.allUserEmails) }, /** @@ -314,10 +345,38 @@ export default { * @return {string|undefined} */ existingParticipationStatus() { - const attendee = findAttendee(this.existingVEvent, this.currentUserPrincipalEmail) + const attendee = findAttendeeByEmails(this.existingVEvent, this.allUserEmails) return attendee?.participationStatus ?? undefined }, + /** + * All user's email addresses (principal + all mail account addresses and aliases) + * + * @return {string[]} + */ + allUserEmails() { + const emails = new Set() + if (this.currentUserPrincipalEmail) { + emails.add(this.currentUserPrincipalEmail.toLowerCase()) + } + if (Array.isArray(this.accounts)) { + for (const account of this.accounts) { + if (account?.emailAddress) { + emails.add(String(account.emailAddress).toLowerCase()) + } + if (Array.isArray(account?.aliases)) { + for (const alias of account.aliases) { + const address = alias?.alias || alias?.emailAddress + if (address) { + emails.add(String(address).toLowerCase()) + } + } + } + } + } + return Array.from(emails) + }, + /** * The status message to show in case of REPLY messages. * @@ -352,6 +411,14 @@ export default { }) }, + existingEventFetched: { + immediate: false, + async handler(fetched) { + if (!fetched) return + await this.autoCreateTentativeIfNeeded() + }, + }, + /** * List of calendar options for the target calendar picker. * @@ -410,6 +477,14 @@ export default { }, }, }, + + async mounted() { + // If data already fetched on mount, attempt auto-create once + if (this.existingEventFetched) { + await this.autoCreateTentativeIfNeeded() + } + }, + methods: { async accept() { await this.saveEventWithParticipationStatus(ACCEPTED) @@ -420,6 +495,22 @@ export default { async decline() { await this.saveEventWithParticipationStatus(DECLINED) }, + async autoCreateTentativeIfNeeded() { + try { + if ( + this.isRequest + && !this.wasProcessed + && this.userIsAttendee + && this.eventIsInFuture + && this.existingEventFetched + && !this.isExistingEvent + ) { + await this.saveEventWithParticipationStatus(TENTATIVE) + } + } catch (e) { + // ignore auto-create failures + } + }, async saveEventWithParticipationStatus(status) { let vCalendar if (this.isExistingEvent) { @@ -428,7 +519,7 @@ export default { vCalendar = this.attachedVCalendar } const vEvent = vCalendar.getFirstComponent('VEVENT') - const attendee = findAttendee(vEvent, this.currentUserPrincipalEmail) + const attendee = findAttendeeByEmails(vEvent, this.allUserEmails) if (!attendee) { return } diff --git a/src/components/Message.vue b/src/components/Message.vue index 8ede00d289..474bc4602b 100644 --- a/src/components/Message.vue +++ b/src/components/Message.vue @@ -23,7 +23,9 @@