Skip to content

Commit 74f85e9

Browse files
committed
feat: list attachements
Signed-off-by: Hamza <[email protected]>
1 parent 14db134 commit 74f85e9

File tree

8 files changed

+207
-7
lines changed

8 files changed

+207
-7
lines changed

lib/Db/Message.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ class Message extends Entity implements JsonSerializable {
142142

143143
/** @var bool */
144144
private $fetchAvatarFromClient = false;
145+
/** @var array */
146+
private $attachments = [];
145147

146148
public function __construct() {
147149
$this->from = new AddressList([]);
@@ -312,6 +314,14 @@ public function getAvatar(): ?Avatar {
312314
return $this->avatar;
313315
}
314316

317+
public function setAttachments(array $attachments): void {
318+
$this->attachments = $attachments;
319+
}
320+
321+
public function getAttachments(): array {
322+
return $this->attachments;
323+
}
324+
315325
#[\Override]
316326
#[ReturnTypeWillChange]
317327
public function jsonSerialize() {
@@ -357,6 +367,7 @@ public function jsonSerialize() {
357367
'mentionsMe' => $this->getMentionsMe(),
358368
'avatar' => $this->avatar?->jsonSerialize(),
359369
'fetchAvatarFromClient' => $this->fetchAvatarFromClient,
370+
'attachments' => $this->getAttachments(),
360371
];
361372
}
362373
}

lib/IMAP/PreviewEnhancer.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCA\Mail\Db\Message;
1616
use OCA\Mail\Db\MessageMapper as DbMapper;
1717
use OCA\Mail\IMAP\MessageMapper as ImapMapper;
18+
use OCA\Mail\Service\Attachment\AttachmentService;
1819
use OCA\Mail\Service\Avatar\Avatar;
1920
use OCA\Mail\Service\AvatarService;
2021
use Psr\Log\LoggerInterface;
@@ -39,11 +40,14 @@ class PreviewEnhancer {
3940
/** @var AvatarService */
4041
private $avatarService;
4142

42-
public function __construct(IMAPClientFactory $clientFactory,
43+
public function __construct(
44+
IMAPClientFactory $clientFactory,
4345
ImapMapper $imapMapper,
4446
DbMapper $dbMapper,
4547
LoggerInterface $logger,
46-
AvatarService $avatarService) {
48+
AvatarService $avatarService,
49+
private AttachmentService $attachmentService,
50+
) {
4751
$this->clientFactory = $clientFactory;
4852
$this->imapMapper = $imapMapper;
4953
$this->mapper = $dbMapper;
@@ -65,6 +69,12 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo
6569

6670
return array_merge($carry, [$message->getUid()]);
6771
}, []);
72+
$client = $this->clientFactory->getClient($account);
73+
74+
foreach ($messages as $message) {
75+
$attachments = $this->attachmentService->getAttachmentNames($account, $mailbox, $message, $client);
76+
$message->setAttachments($attachments);
77+
}
6878

6979
if ($preLoadAvatars) {
7080
foreach ($messages as $message) {
@@ -87,7 +97,7 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo
8797
return $messages;
8898
}
8999

90-
$client = $this->clientFactory->getClient($account);
100+
91101
try {
92102
$data = $this->imapMapper->getBodyStructureData(
93103
$client,

lib/Model/IMAPMessage.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ public function getSentDate(): Horde_Imap_Client_DateTime {
287287
return $this->imapDate;
288288
}
289289

290+
290291
/**
291292
* @param int $id
292293
*
@@ -386,7 +387,7 @@ public function setContent(string $content) {
386387
*/
387388
#[\Override]
388389
public function getAttachments(): array {
389-
throw new Exception('not implemented');
390+
return $this->attachments;
390391
}
391392

392393
/**

lib/Service/Attachment/AttachmentService.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
use OCA\Mail\Db\LocalAttachment;
1919
use OCA\Mail\Db\LocalAttachmentMapper;
2020
use OCA\Mail\Db\LocalMessage;
21+
use OCA\Mail\Db\Mailbox;
22+
use OCA\Mail\Db\Message;
2123
use OCA\Mail\Exception\AttachmentNotFoundException;
24+
use OCA\Mail\Exception\ServiceException;
2225
use OCA\Mail\Exception\UploadException;
2326
use OCA\Mail\IMAP\MessageMapper;
2427
use OCP\AppFramework\Db\DoesNotExistException;
2528
use OCP\Files\File;
2629
use OCP\Files\Folder;
2730
use OCP\Files\NotFoundException;
2831
use OCP\Files\NotPermittedException;
32+
use OCP\ICacheFactory;
2933
use Psr\Log\LoggerInterface;
3034

3135
class AttachmentService implements IAttachmentService {
@@ -51,6 +55,10 @@ class AttachmentService implements IAttachmentService {
5155
* @var LoggerInterface
5256
*/
5357
private $logger;
58+
/**
59+
* @var ICache
60+
*/
61+
private $cache;
5462

5563
/**
5664
* @param Folder $userFolder
@@ -60,13 +68,15 @@ public function __construct($userFolder,
6068
AttachmentStorage $storage,
6169
IMailManager $mailManager,
6270
MessageMapper $imapMessageMapper,
71+
ICacheFactory $cacheFactory,
6372
LoggerInterface $logger) {
6473
$this->mapper = $mapper;
6574
$this->storage = $storage;
6675
$this->mailManager = $mailManager;
6776
$this->messageMapper = $imapMessageMapper;
6877
$this->userFolder = $userFolder;
6978
$this->logger = $logger;
79+
$this->cache = $cacheFactory->createLocal('mail.attachment_names');
7080
}
7181

7282
/**
@@ -247,6 +257,32 @@ public function handleAttachments(Account $account, array $attachments, \Horde_I
247257
return array_values(array_filter($attachmentIds));
248258
}
249259

260+
public function getAttachmentNames(Account $account, Mailbox $mailbox, Message $message, \Horde_Imap_Client_Socket $client): array {
261+
$attachments = [];
262+
$uniqueCacheId = $account->getUserId() . $account->getId() . $mailbox->getId() . $message->getUid();
263+
$cached = $this->cache->get($uniqueCacheId);
264+
if ($cached) {
265+
return $cached;
266+
}
267+
try {
268+
$imapMessage = $this->mailManager->getImapMessage(
269+
$client,
270+
$account,
271+
$mailbox,
272+
$message->getUid(),
273+
true
274+
);
275+
$attachments = $imapMessage->getAttachments();
276+
} catch (ServiceException $e) {
277+
$this->logger->error('Could not get attachment names', ['exception' => $e, 'messageId' => $message->getUid()]);
278+
}
279+
$result = array_map(static function (array $attachment) {
280+
return ['name' => $attachment['fileName'],'mime' => $attachment['mime']];
281+
}, $attachments);
282+
$this->cache->set($uniqueCacheId, $result);
283+
return $result;
284+
}
285+
250286
/**
251287
* Add a message as attachment
252288
*

src/components/AttachmentTag.vue

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<div class="attachment-tag">
7+
<FileIcon :file-name="fileName" :mime-type="mimeType" />
8+
<p class="attachment-tag__filename">
9+
{{ fileName }}
10+
</p>
11+
</div>
12+
</template>
13+
14+
<script>
15+
import FileIcon from './icons/FileIcon.vue'
16+
export default {
17+
name: 'AttachmentTag',
18+
components: { FileIcon },
19+
props: {
20+
fileName: {
21+
type: String,
22+
required: true,
23+
},
24+
mimeType: {
25+
type: String,
26+
required: true,
27+
},
28+
},
29+
}
30+
</script>
31+
<style scoped>
32+
.attachment-tag {
33+
height: 24px;
34+
border: 1px solid var(--color-border-dark);
35+
border-radius: var(--border-radius-element);
36+
gap: 4px;
37+
display: flex;
38+
align-items: center;
39+
width: max-content;
40+
margin-inline-end: 4px;
41+
padding: 0 4px;
42+
}
43+
</style>

src/components/Envelope.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,10 @@
399399
{{ translateTagDisplayName(tag) }}
400400
</span>
401401
</div>
402-
<MoveModal
403-
v-if="showMoveModal"
402+
<div v-for="(attachment,id) in attachments" :key="id">
403+
<AttachmentTag :file-name="attachment.name" :mime-type="attachment.mime" />
404+
</div>
405+
<MoveModal v-if="showMoveModal"
404406
:account="account"
405407
:envelopes="[data]"
406408
:move-thread="listViewThreaded"
@@ -485,10 +487,12 @@ import { mailboxHasRights } from '../util/acl.js'
485487
import { messageDateTime, shortRelativeDatetime } from '../util/shortRelativeDatetime.js'
486488
import { translateTagDisplayName } from '../util/tag.js'
487489
import { hiddenTags } from './tags.js'
490+
import AttachmentTag from './AttachmentTag.vue'
488491
489492
export default {
490493
name: 'Envelope',
491494
components: {
495+
AttachmentTag,
492496
AlertOctagonIcon,
493497
Avatar,
494498
IconCreateEvent,
@@ -718,7 +722,9 @@ export default {
718722
719723
return tags
720724
},
721-
725+
attachments() {
726+
return Object.values(this.threadList).map((envelope) => (envelope?.attachments)).flat()
727+
},
722728
draggableLabel() {
723729
let label = this.data.subject
724730
const sender = this.data.from[0]?.label ?? this.data.from[0]?.email

src/components/icons/FileIcon.vue

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<span class="file-icon" :style="{ color: `var(${color})` }">
7+
<component :is="iconComponent" class="file-icon__svg" :size="16" />
8+
</span>
9+
</template>
10+
11+
<script>
12+
import { FILE_EXTENSIONS_WORD_PROCESSING, FILE_EXTENSIONS_SPREADSHEET, FILE_EXTENSIONS_PRESENTATION } from '../../store/constants.js'
13+
import FileOutlineIcon from 'vue-material-design-icons/FileOutline.vue'
14+
import ImageOutlineIcon from 'vue-material-design-icons/ImageOutline.vue'
15+
import VideoOutlineIcon from 'vue-material-design-icons/FileVideoOutline.vue'
16+
import MusicOutlineIcon from 'vue-material-design-icons/FileMusicOutline.vue'
17+
import DocumentOutlineIcon from 'vue-material-design-icons/FileDocumentOutline.vue'
18+
import FilePdfBox from 'vue-material-design-icons/FilePdfBox.vue'
19+
20+
export default {
21+
name: 'FileIcon',
22+
props: {
23+
fileName: {
24+
type: String,
25+
required: true,
26+
},
27+
mimeType: {
28+
type: String,
29+
required: true,
30+
},
31+
},
32+
data() {
33+
return {
34+
extension: '',
35+
icon: null,
36+
color: '--color-text-maxcontrast',
37+
}
38+
},
39+
computed: {
40+
iconName() {
41+
const type = this.mimeType.split('/')[0].toLowerCase()
42+
if (this.extension === 'pdf') return 'pdf'
43+
if ([...FILE_EXTENSIONS_WORD_PROCESSING, 'txt', 'md'].includes(this.extension)) return 'document'
44+
if (type === 'image') return 'image'
45+
if (type === 'video') return 'video'
46+
if (type === 'audio') return 'music'
47+
return 'file'
48+
},
49+
iconComponent() {
50+
const map = {
51+
file: FileOutlineIcon,
52+
image: ImageOutlineIcon,
53+
video: VideoOutlineIcon,
54+
music: MusicOutlineIcon,
55+
document: DocumentOutlineIcon,
56+
pdf: FilePdfBox,
57+
}
58+
return map[this.iconName] || FileOutlineIcon
59+
},
60+
},
61+
mounted() {
62+
this.extension = this.fileName.split('.').pop().toLowerCase()
63+
this.setColor()
64+
},
65+
methods: {
66+
setColor() {
67+
if (FILE_EXTENSIONS_WORD_PROCESSING.includes(this.extension)) {
68+
this.color = '--color-info-text'
69+
} else if (FILE_EXTENSIONS_SPREADSHEET.includes(this.extension)) {
70+
this.color = '--color-border-success'
71+
} else if (FILE_EXTENSIONS_PRESENTATION.includes(this.extension)) {
72+
this.color = '--color-favorite'
73+
} else if (this.extension === 'pdf') {
74+
this.color = '--color-text-error'
75+
}
76+
},
77+
},
78+
}
79+
</script>
80+
<style scoped>
81+
.file-icon {
82+
display: inline-flex;
83+
align-items: center;
84+
justify-content: center;
85+
width: 16px;
86+
height: 16px;
87+
border-radius: 4px;
88+
padding: 0 4px;
89+
}
90+
</style>

src/store/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ export const STATUS_IMAP_SENT_MAILBOX_FAIL = 11
1818
export const STATUS_SMTP_ERROR = 13
1919

2020
export const FOLLOW_UP_TAG_LABEL = '$follow_up'
21+
export const FILE_EXTENSIONS_WORD_PROCESSING = ['doc', 'docx', 'dot', 'odt', 'dotx', 'odt', 'ott']
22+
export const FILE_EXTENSIONS_SPREADSHEET = ['xls', 'xlsx', 'ods']
23+
export const FILE_EXTENSIONS_PRESENTATION = ['ppt', 'pptx', 'odp', 'otp', 'pps', 'ppsx', 'pot', 'potx']

0 commit comments

Comments
 (0)