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
8 changes: 8 additions & 0 deletions lib/Contracts/IMailManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,12 @@ public function updateSubscription(Account $account,
*/
public function enableMailboxBackgroundSync(Mailbox $mailbox,
bool $syncInBackground): Mailbox;

/**
* @param Account $account
* @param Mailbox $mailbox
* @param Message $message
* @return array
*/
public function getMailAttachments(Account $account, Mailbox $mailbox, Message $message) : array;
}
38 changes: 38 additions & 0 deletions lib/Controller/MessagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\ZipResponse;
use OCP\Files\Folder;
use OCP\Files\IMimeTypeDetector;
use OCP\IL10N;
Expand Down Expand Up @@ -525,6 +526,43 @@ public function downloadAttachment(int $id,
);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
* @TrapError
*
* @param int $id the message id
* @param string $attachmentId
*
* @return ZipResponse|JSONResponse
*
* @throws ClientException
* @throws ServiceException
* @throws DoesNotExistException
*/
public function downloadAttachments(int $id): Response {
try {
$message = $this->mailManager->getMessage($this->currentUserId, $id);
$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
} catch (DoesNotExistException $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}

$attachments = $this->mailManager->getMailAttachments($account, $mailbox, $message);
$zip = new ZipResponse($this->request, 'attachments');

foreach ($attachments as $attachment) {
$fileName = $attachment['name'];
$fh = fopen("php://temp", 'r+');
fputs($fh, $attachment['content']);
$size = (int)$attachment['size'];
rewind($fh);
$zip->addResource($fh, $fileName, $size);
}
return $zip;
}

/**
* @NoAdminRequired
* @TrapError
Expand Down
107 changes: 89 additions & 18 deletions lib/IMAP/MessageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -471,28 +471,65 @@ public function getRawAttachments(Horde_Imap_Client_Socket $client,
}

$structure = $structureResult->getStructure();
$partsQuery = new Horde_Imap_Client_Fetch_Query();
$partsQuery->fullText();
foreach ($structure->partIterator() as $part) {
$partsQuery = $this->buildAttachmentsPartsQuery($structure, $attachmentIds);

$parts = $client->fetch($mailbox, $partsQuery, [
'ids' => new Horde_Imap_Client_Ids([$uid]),
]);
if (($messageData = $parts->first()) === null) {
throw new DoesNotExistException('Message does not exist');
}

$attachments = [];
foreach ($structure->partIterator() as $key => $part) {
/** @var Horde_Mime_Part $part */
if ($part->getMimeId() === '0') {
// Ignore message header
continue;
}
if (!empty($attachmentIds) && !in_array($part->getMIMEId(), $attachmentIds, true)) {
// We are looking for specific parts only and this is not one of them

if (!$part->isAttachment()) {
continue;
}

$partsQuery->bodyPart($part->getMimeId(), [
'peek' => true,
]);
$partsQuery->mimeHeader($part->getMimeId(), [
'peek' => true
$stream = $messageData->getBodyPart($key, true);
$mimeHeaders = $messageData->getMimeHeader($key, Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) {
$part->setTransferEncoding($enc);
}
$part->setContents($stream, [
'usestream' => true,
]);
$partsQuery->bodyPartSize($part->getMimeId());
$decoded = $part->getContents();

$attachments[] = $decoded;
}
return $attachments;
}

/**
* Get Attachments with size, content and name properties
*
* @param Horde_Imap_Client_Socket $client
* @param string $mailbox
* @param integer $uid
* @param array|null $attachmentIds
* @return array[]
*/
public function getAttachments(Horde_Imap_Client_Socket $client,
string $mailbox,
int $uid,
?array $attachmentIds = []): array {
$messageQuery = new Horde_Imap_Client_Fetch_Query();
$messageQuery->structure();

$result = $client->fetch($mailbox, $messageQuery, [
'ids' => new Horde_Imap_Client_Ids([$uid]),
]);

if (($structureResult = $result->first()) === null) {
throw new DoesNotExistException('Message does not exist');
}

$structure = $structureResult->getStructure();
$partsQuery = $this->buildAttachmentsPartsQuery($structure, $attachmentIds);

$parts = $client->fetch($mailbox, $partsQuery, [
'ids' => new Horde_Imap_Client_Ids([$uid]),
]);
Expand All @@ -516,13 +553,47 @@ public function getRawAttachments(Horde_Imap_Client_Socket $client,
$part->setContents($stream, [
'usestream' => true,
]);
$decoded = $part->getContents();

$attachments[] = $decoded;
$attachments[] = [
'content' => $part->getContents(),
'name' => $part->getName(),
'size' => $part->getSize()
];
}
return $attachments;
}

/**
* Build the parts query for attachments
*
* @param $structure
* @param array $attachmentIds
* @return Horde_Imap_Client_Fetch_Query
*/
private function buildAttachmentsPartsQuery($structure, array $attachmentIds) : Horde_Imap_Client_Fetch_Query {
$partsQuery = new Horde_Imap_Client_Fetch_Query();
$partsQuery->fullText();
foreach ($structure->partIterator() as $part) {
/** @var Horde_Mime_Part $part */
if ($part->getMimeId() === '0') {
// Ignore message header
continue;
}
if (!empty($attachmentIds) && !in_array($part->getMIMEId(), $attachmentIds, true)) {
// We are looking for specific parts only and this is not one of them
continue;
}

$partsQuery->bodyPart($part->getMimeId(), [
'peek' => true,
]);
$partsQuery->mimeHeader($part->getMimeId(), [
'peek' => true
]);
$partsQuery->bodyPartSize($part->getMimeId());
}
return $partsQuery;
}

/**
* @param Horde_Imap_Client_Socket $client
* @param int[] $uids
Expand Down
10 changes: 10 additions & 0 deletions lib/Service/MailManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -502,4 +502,14 @@ public function deleteMailbox(Account $account,
$this->folderMapper->delete($client, $mailbox->getName());
$this->mailboxMapper->delete($mailbox);
}

/**
* @param Account $account
* @param Mailbox $mailbox
* @param Message $message
* @return array[]
*/
public function getMailAttachments(Account $account, Mailbox $mailbox, Message $message) : array {
return $this->imapMessageMapper->getAttachments($this->imapClientFactory->getClient($account), $mailbox->getName(), $message->getUid());
}
}
2 changes: 1 addition & 1 deletion src/components/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
{{ t('mail', 'Attachments') }}
</ActionButton>
</Actions>
<MessageAttachments v-close-popover="true" :attachments="message.attachments" />
<MessageAttachments v-close-popover="true" :attachments="message.attachments" :envelope="envelope" />
</Popover>
<div id="reply-composer" />
</div>
Expand Down
19 changes: 19 additions & 0 deletions src/components/MessageAttachments.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@
@click="saveAll">
{{ t('mail', 'Save all to Files') }}
</button>
<button
class="attachments-save-to-cloud icon-folder"
@click="downloadZip">
{{ t('mail', 'Download ZIP') }}
</button>
</p>
</div>
</template>

<script>
import { generateUrl } from '@nextcloud/router'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { saveAttachmentsToFiles } from '../service/AttachmentService'

Expand All @@ -59,6 +65,10 @@ export default {
MessageAttachment,
},
props: {
envelope: {
required: true,
type: Object,
},
attachments: {
type: Array,
required: true,
Expand All @@ -73,6 +83,11 @@ export default {
moreThanOne() {
return this.attachments.length > 1
},
zipUrl() {
return generateUrl('/apps/mail/api/messages/{id}/attachments', {
id: this.envelope.databaseId,
})
},
},
methods: {
saveAll() {
Expand Down Expand Up @@ -100,6 +115,10 @@ export default {
.catch((error) => Logger.error('not saved', { error }))
.then(() => (this.savingToCloud = false))
},
downloadZip() {
window.open(this.zipUrl)
window.focus()
},
},
}
</script>
Expand Down
Loading