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
61 changes: 40 additions & 21 deletions lib/Controller/MessagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,37 @@ public function downloadAttachments(int $id): Response {
return $zip;
}

private function moveAttachmentToFiles(Attachment $attachment, string $targetPath, ?string $fileName): void {
if ($this->userFolder === null) {
return;
}

if ($fileName === null) {
$fileName = '';
} else {
$fileName = trim($fileName);
}

if ($fileName === '') {
$fileName = $attachment->getName() ?? $this->l10n->t('Embedded message %s', [
$attachment->getId(),
]) . '.eml';
}

$fileParts = pathinfo($fileName);
$fileName = $fileParts['filename'];
$fileExtension = $fileParts['extension'] ?? '';
$fullPath = "$targetPath/$fileName.$fileExtension";
$counter = 2;
while ($this->userFolder->nodeExists($fullPath)) {
$fullPath = "$targetPath/$fileName ($counter).$fileExtension";
$counter++;
}

$newFile = $this->userFolder->newFile($fullPath);
$newFile->putContent($attachment->getContent());
}

/**
* @NoAdminRequired
*
Expand All @@ -858,7 +889,8 @@ public function downloadAttachments(int $id): Response {
#[TrapError]
public function saveAttachment(int $id,
string $attachmentId,
string $targetPath) {
string $targetPath,
?string $fileName) {
if ($this->userId === null) {
return new JSONResponse([], Http::STATUS_UNAUTHORIZED);
}
Expand All @@ -880,40 +912,27 @@ public function saveAttachment(int $id,
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}

/** @var Attachment[] $attachments */
$attachments = [];
if ($attachmentId === '0') {
$attachments = $this->mailManager->getMailAttachments(
$account,
$mailbox,
$message,
);

foreach ($attachments as $attachment) {
$this->moveAttachmentToFiles($attachment, $targetPath, null);
}
} else {
$attachments[] = $this->mailManager->getMailAttachment(
$attachment = $this->mailManager->getMailAttachment(
$account,
$mailbox,
$message,
$attachmentId,
);
}

foreach ($attachments as $attachment) {
$fileName = $attachment->getName() ?? $this->l10n->t('Embedded message %s', [
$attachment->getId(),
]) . '.eml';
$fileParts = pathinfo($fileName);
$fileName = $fileParts['filename'];
$fileExtension = $fileParts['extension'] ?? '';
$fullPath = "$targetPath/$fileName.$fileExtension";
$counter = 2;
while ($this->userFolder->nodeExists($fullPath)) {
$fullPath = "$targetPath/$fileName ($counter).$fileExtension";
$counter++;
}

$newFile = $this->userFolder->newFile($fullPath);
$newFile->putContent($attachment->getContent());
$this->moveAttachmentToFiles($attachment, $targetPath, $fileName);
}

return new JSONResponse();
}

Expand Down
59 changes: 54 additions & 5 deletions src/components/MessageAttachment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</span>
<span class="attachment-size">{{ humanReadable(size) }}</span>
</div>

<FilePicker
v-if="isFilePickerOpen"
:name="t('mail', 'Choose a folder to store the attachment in')"
Expand All @@ -27,6 +28,17 @@
:multiselect="false"
:mimetype-filter="['httpd/unix-directory']"
@close="() => isFilePickerOpen = false" />

<NcDialog
:open.sync="isRenameOpen"
:name="t('mail', 'Rename')"
:buttons="renameButtons">
<NcInputField
v-model="newFileName"
:label="t('mail', 'Filename')"
required />
</NcDialog>

<Actions :boundaries-element="boundariesElement">
<template v-if="!showCalendarPopover">
<ActionButton
Expand Down Expand Up @@ -85,7 +97,7 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
import { FilePickerVue as FilePicker } from '@nextcloud/dialogs/filepicker.js'
import { formatFileSize } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { NcActionButton as ActionButton, NcActions as Actions, NcLoadingIcon as IconLoading } from '@nextcloud/vue'
import { NcActionButton as ActionButton, NcActions as Actions, NcLoadingIcon as IconLoading, NcDialog, NcInputField } from '@nextcloud/vue'
import IconArrow from 'vue-material-design-icons/ArrowLeft.vue'
import IconSave from 'vue-material-design-icons/FolderOutline.vue'
import IconAdd from 'vue-material-design-icons/Plus.vue'
Expand All @@ -98,6 +110,8 @@ export default {
name: 'MessageAttachment',
components: {
FilePicker,
NcDialog,
NcInputField,
Actions,
ActionButton,
IconAdd,
Expand Down Expand Up @@ -163,13 +177,32 @@ export default {
showCalendarPopover: false,
saveAttachementButtons: [
{
label: t('mail', 'Choose'),
label: t('mail', 'Rename and save'),
callback: this.openRename,
type: 'secondary',
},
{
label: t('mail', 'Save as {filename}', {
filename: this.fileName,
}),

callback: this.saveToCloud,
type: 'primary',
},
],

renameButtons: [
{
label: t('mail', 'Save'),
callback: this.saveRename,
type: 'primary',
},
],

path: '',
isFilePickerOpen: false,
newFileName: this.fileName,
isRenameOpen: false,
}
},

Expand Down Expand Up @@ -225,13 +258,29 @@ export default {
return formatFileSize(size)
},

async saveToCloud(dest) {
const path = dest[0].path
openRename(dest) {
this.path = dest[0].path
this.newFileName = this.fileName
this.isRenameOpen = true
},

saveRename() {
this.isRenameOpen = false
this.realSave()
},

saveToCloud(dest) {
this.path = dest[0].path
this.newFileName = this.fileName
this.realSave()
},

async realSave() {
this.savingToCloud = true
const id = this.$route.params.threadId

try {
await saveAttachmentToFiles(id, this.id, path)
await saveAttachmentToFiles(id, this.id, this.path, this.newFileName)
Logger.info('saved')
showSuccess(t('mail', 'Attachment saved to Files'))
} catch (e) {
Expand Down
5 changes: 3 additions & 2 deletions src/service/AttachmentService.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import Axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'

export async function saveAttachmentToFiles(id, attachmentId, directory) {
export async function saveAttachmentToFiles(id, attachmentId, directory, filename) {
const url = generateUrl(
'/apps/mail/api/messages/{id}/attachment/{attachmentId}',
{
Expand All @@ -17,12 +17,13 @@ export async function saveAttachmentToFiles(id, attachmentId, directory) {

return await Axios.post(url, {
targetPath: directory,
fileName: filename,
})
}

export async function saveAttachmentsToFiles(id, directory) {
// attachmentId = 0 means 'all attachments' (see MessageController.php::saveAttachement)
return await saveAttachmentToFiles(id, 0, directory)
return await saveAttachmentToFiles(id, 0, directory, null)
}

export function downloadAttachment(url) {
Expand Down
73 changes: 69 additions & 4 deletions tests/Unit/Controller/MessagesControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,71 @@ public function testSaveSingleAttachment() {
$response = $this->controller->saveAttachment(
$id,
$attachmentId,
$targetPath
$targetPath,
null
);

$this->assertEquals($expected, $response);
}

public function testSaveAndRenameSingleAttachment() {
$accountId = 17;
$mailboxId = 987;
$id = 123;
$uid = 321;
$attachmentId = '2.2';
$targetPath = 'Downloads';
$message = new \OCA\Mail\Db\Message();
$message->setMailboxId($mailboxId);
$message->setUid($uid);
$mailbox = new \OCA\Mail\Db\Mailbox();
$mailbox->setName('INBOX');
$mailbox->setAccountId($accountId);
$this->mailManager->expects($this->once())
->method('getMessage')
->with($this->userId, $id)
->willReturn($message);
$this->mailManager->expects($this->once())
->method('getMailbox')
->with($this->userId, $mailboxId)
->willReturn($mailbox);
$this->accountService->expects($this->once())
->method('find')
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
->method('getMailAttachment')
->with($this->account, $mailbox, $message, $attachmentId)
->will($this->returnValue($this->attachment));
$folderNode = $this->createStub(Folder::class);
$this->userFolder->expects($this->once())
->method('get')
->with('Downloads')
->willReturn($folderNode);
$this->userFolder->expects($this->exactly(2))
->method('nodeExists')
->withConsecutive(['Downloads'], ['Downloads/foobar.jpg'])
->willReturnOnConsecutiveCalls(true, false);
$file = $this->getMockBuilder('\OCP\Files\File')
->disableOriginalConstructor()
->getMock();
$this->userFolder->expects($this->once())
->method('newFile')
->with('Downloads/foobar.jpg')
->will($this->returnValue($file));
$file->expects($this->once())
->method('putContent')
->with('abcdefg');
$this->attachment->expects($this->once())
->method('getContent')
->will($this->returnValue('abcdefg'));

$expected = new JSONResponse();
$response = $this->controller->saveAttachment(
$id,
$attachmentId,
$targetPath,
'foobar.jpg'
);

$this->assertEquals($expected, $response);
Expand Down Expand Up @@ -476,7 +540,8 @@ public function testSaveAllAttachments() {
$response = $this->controller->saveAttachment(
$id,
$attachmentId,
$targetPath
$targetPath,
null
);

$this->assertEquals($expected, $response);
Expand All @@ -488,7 +553,7 @@ public function testSaveAttachmentTargetPathNotFound(): void {
->with('NoSuchFolder')
->willReturn(false);

$response = $this->controller->saveAttachment(123, '1', 'NoSuchFolder');
$response = $this->controller->saveAttachment(123, '1', 'NoSuchFolder', null);

$this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
Expand All @@ -506,7 +571,7 @@ public function testSaveAttachmentTargetPathIsFile(): void {
->with('some/file.txt')
->willReturn($fileNode);

$response = $this->controller->saveAttachment(123, '1', 'some/file.txt');
$response = $this->controller->saveAttachment(123, '1', 'some/file.txt', null);

$this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
Expand Down
Loading