Skip to content
Merged
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
54 changes: 40 additions & 14 deletions lib/Model/IMAPMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public function __construct($conn,
public $plainMessage = '';
public $attachments = [];
public $inlineAttachments = [];
public $scheduling = [];
private $loadHtmlMessage = false;
private $hasHtmlMessage = false;

Expand Down Expand Up @@ -398,6 +399,44 @@ private function loadMessageBodies(): void {
* @return void
*/
private function getPart(Horde_Mime_Part $p, $partNo): void {
// iMIP messages
// Handle text/calendar parts first because they might be attachments at the same time.
// Otherwise, some of the following if-conditions might break the handling and treat iMIP
// data like regular attachments.
$allContentTypeParameters = $p->getAllContentTypeParameters();
if ($p->getType() === 'text/calendar') {
// Handle event data like a regular attachment
// Outlook doesn't set a content disposition
// We work around this by checking for the name only
if ($p->getName() !== null) {
$this->attachments[] = [
'id' => $p->getMimeId(),
'messageId' => $this->messageId,
'fileName' => $p->getName(),
'mime' => $p->getType(),
'size' => $p->getBytes(),
'cid' => $p->getContentId(),
'disposition' => $p->getDisposition()
];
}

// return if this is an event attachment only
// the method parameter determines if this is a iMIP message
if (!isset($allContentTypeParameters['method'])) {
return;
}

if (in_array(strtoupper($allContentTypeParameters['method']), ['REQUEST', 'REPLY', 'CANCEL'])) {
$this->scheduling[] = [
'id' => $p->getMimeId(),
'messageId' => $this->messageId,
'method' => strtoupper($allContentTypeParameters['method']),
'contents' => $this->loadBodyData($p, $partNo),
];
return;
}
}

// Regular attachments
if ($p->isAttachment() || $p->getType() === 'message/rfc822') {
$this->attachments[] = [
Expand Down Expand Up @@ -442,19 +481,6 @@ private function getPart(Horde_Mime_Part $p, $partNo): void {
return;
}

if ($p->getType() === 'text/calendar') {
$this->attachments[] = [
'id' => $p->getMimeId(),
'messageId' => $this->messageId,
'fileName' => $p->getName() ?? 'calendar.ics',
'mime' => $p->getType(),
'size' => $p->getBytes(),
'cid' => $p->getContentId(),
'disposition' => $p->getDisposition()
];
return;
}

if ($p->getType() === 'text/html') {
$this->handleHtmlMessage($p, $partNo);
return;
Expand All @@ -478,7 +504,6 @@ private function getPart(Horde_Mime_Part $p, $partNo): void {
*/
public function getFullMessage(int $id): array {
$mailBody = $this->plainMessage;

$data = $this->jsonSerialize();
if ($this->hasHtmlMessage) {
$data['hasHtmlBody'] = true;
Expand Down Expand Up @@ -509,6 +534,7 @@ public function jsonSerialize() {
'flags' => $this->getFlags(),
'hasHtmlBody' => $this->hasHtmlMessage,
'dispositionNotificationTo' => $this->getDispositionNotificationTo(),
'scheduling' => $this->scheduling,
];
}

Expand Down
181 changes: 181 additions & 0 deletions src/components/Imip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<!--
- @copyright Copyright (c) 2022 Richard Steinmetz <[email protected]>
-
- @author Richard Steinmetz <[email protected]>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->

<template>
<div class="imip">
<div
v-if="isRequest"
class="imip__type">
<span v-if="wasUpdated">{{ t('mail', 'An event you have been invited to was updated') }}</span>
<span v-else>{{ t('mail', 'You have been invited to an event') }}</span>
</div>
<div
v-else-if="isReply"
class="imip__type">
<CalendarIcon :size="20" />
<span>{{ t('mail', 'This event was updated') }}</span>
</div>
<div
v-else-if="isCancel"
class="imip__type">
<CloseIcon :size="20" fill-color="red" />
<span>{{ t('mail', 'This event was cancelled') }}</span>
</div>

<EventData :event="parsedEvent" />

<!-- TODO: actually implement buttons (https://github.com/nextcloud/mail/issues/6803) -->
<!-- TODO: "More options" needs more specification -->
<!--
<div class="imip__actions">
<template v-if="isRequest && eventIsInFuture">
<Button
type="tertiary">
{{ t('mail', 'Accept') }}
</Button>
<Button type="tertiary">
{{ t('mail', 'Decline') }}
</Button>
</template>
<Actions :menu-title="t('mail', 'More options')" />
</div>
-->
</div>
</template>

<script>
import EventData from './imip/EventData'
// import Button from '@nextcloud/vue/dist/Components/Button'
// import Actions from '@nextcloud/vue/dist/Components/Actions'
import CloseIcon from 'vue-material-design-icons/Close'
import CalendarIcon from 'vue-material-design-icons/Calendar'
import { getParserManager } from '@nextcloud/calendar-js'

const REQUEST = 'REQUEST'
const REPLY = 'REPLY'
const CANCEL = 'CANCEL'

export default {
name: 'Imip',
components: {
EventData,
// Button,
// Actions,
CloseIcon,
CalendarIcon,
},
props: {
scheduling: {
type: Object,
required: true,
},
},
data() {
return {
REQUEST,
CANCEL,
REPLY,
}
},
computed: {
/**
* @returns {string}
*/
method() {
return this.scheduling.method
},

/**
* @returns {boolean}
*/
isRequest() {
return this.method === REQUEST
},

/**
* @returns {boolean}
*/
isReply() {
return this.method === REPLY
},

/**
* @returns {boolean}
*/
isCancel() {
return this.method === CANCEL
},

/**
* @returns {boolean}
*/
wasUpdated() {
// TODO: ask backend whether invitation is new or was updated
return false
},

/**
* @returns {EventComponent|undefined}
*/
parsedEvent() {
const parserManager = getParserManager()
const parser = parserManager.getParserForFileType('text/calendar')
parser.parse(this.scheduling.contents)

const vCalendar = parser.getItemIterator().next().value
if (!vCalendar) {
return undefined
}

const vEvent = vCalendar.getEventIterator().next().value
return vEvent ?? undefined
},

/**
* @returns {boolean}
*/
eventIsInFuture() {
return this.parsedEvent.startDate.jsDate.getTime() > new Date().getTime()
},
},
}
</script>

<style lang="scss" scoped>
.imip {
display: flex;
flex-direction: column;
border: solid 2px var(--color-border);
border-radius: var(--border-radius-large);
padding: 10px;

&__type {
display: flex;
gap: 5px;
}

&__actions {
display: flex;
margin-top: 15px;
}
}
</style>
12 changes: 12 additions & 0 deletions src/components/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
<div v-if="itineraries.length > 0" class="message-itinerary">
<Itinerary :entries="itineraries" :message-id="message.messageId" />
</div>
<div v-if="message.scheduling.length > 0" class="message-imip">
<Imip
v-for="scheduling in message.scheduling"
:key="scheduling.id"
:scheduling="scheduling" />
</div>
<MessageHTMLBody v-if="message.hasHtmlBody"
:url="htmlUrl"
:message="message"
Expand Down Expand Up @@ -54,6 +60,7 @@ import MessageAttachments from './MessageAttachments'
import MessageEncryptedBody from './MessageEncryptedBody'
import MessageHTMLBody from './MessageHTMLBody'
import MessagePlainTextBody from './MessagePlainTextBody'
import Imip from './Imip'

export default {
name: 'Message',
Expand All @@ -63,6 +70,7 @@ export default {
MessageEncryptedBody,
MessageHTMLBody,
MessagePlainTextBody,
Imip,
},
props: {
envelope: {
Expand Down Expand Up @@ -103,4 +111,8 @@ export default {
border-radius: 22px;
background-color: var(--color-background-darker);
}

.message-imip {
padding: 5px 10px;
}
</style>
Loading