Skip to content
Draft
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
55 changes: 50 additions & 5 deletions app/internal_packages/account-sidebar/lib/sidebar-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ import {
Actions,
RegExpUtils,
localized,
MessageStore,
GetMessageRFC2822Task,
MutableQuerySubscription,
Thread,
Message,
DatabaseStore,
Folder,
} from 'mailspring-exports';

import * as SidebarActions from './sidebar-actions';
import { ISidebarItem } from './types';
import { dialog } from '@electron/remote';

const idForCategories = categories => _.pluck(categories, 'id').join('-');

const countForItem = function (perspective) {
const countForItem = function(perspective) {
const unreadCountEnabled = AppEnv.config.get('core.workspace.showUnreadForAllCategories');
if (perspective.isInbox() || unreadCountEnabled) {
return perspective.unreadCount();
Expand All @@ -28,22 +36,22 @@ const countForItem = function (perspective) {

const isItemSelected = perspective => FocusedPerspectiveStore.current().isEqual(perspective);

const isItemCollapsed = function (id) {
const isItemCollapsed = function(id) {
if (AppEnv.savedState.sidebarKeysCollapsed[id] !== undefined) {
return AppEnv.savedState.sidebarKeysCollapsed[id];
} else {
return true;
}
};

const toggleItemCollapsed = function (item) {
const toggleItemCollapsed = function(item) {
if (!(item.children.length > 0)) {
return;
}
SidebarActions.setKeyCollapsed(item.id, !isItemCollapsed(item.id));
};

const onDeleteItem = function (item) {
const onDeleteItem = function(item) {
if (item.deleted === true) {
return;
}
Expand Down Expand Up @@ -74,7 +82,40 @@ const onDeleteItem = function (item) {
);
};

const onEditItem = function (item, value) {
const onExportItem = async function(item) {
const filepath = await dialog.showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
});

if (!filepath.canceled && filepath.filePaths[0]) {
await DatabaseStore.findAll<Message>(Message, { remoteFolderId: this.id }).then(messages => {
const tasks = [];

messages.forEach(message => {
const savePath = `${filepath.filePaths[0]}/${
message.subject
} - ${message.date.toLocaleString('sv-SE')} - ${message.id.substring(0, 10)}.eml`
.replace(/:/g, ';')
.replace(RegExpUtils.illegalPathCharacters(), '-')
.replace(RegExpUtils.unicodeControlCharacters(), '-');

tasks.push(
new GetMessageRFC2822Task({
messageId: message.id,
accountId: message.accountId,
filepath: savePath,
})
);
});

if (tasks.length > 0) {
Actions.queueTasks(tasks);
}
});
}
};

const onEditItem = function(item, value) {
let newDisplayName;
if (!value) {
return;
Expand Down Expand Up @@ -134,6 +175,7 @@ export default class SidebarItem {
counterStyle,
onDelete: opts.deletable ? onDeleteItem : undefined,
onEdited: opts.editable ? onEditItem : undefined,
onExport: opts.exportable ? onExportItem : undefined,
onCollapseToggled: toggleItemCollapsed,

onDrop(item, event) {
Expand Down Expand Up @@ -190,6 +232,9 @@ export default class SidebarItem {
if (opts.editable == null) {
opts.editable = true;
}

opts.exportable = true;

opts.contextMenuLabel = contextMenuLabel;
return this.forPerspective(id, perspective, opts);
}
Expand Down
1 change: 1 addition & 0 deletions app/internal_packages/account-sidebar/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ISidebarItem {

deletable?: boolean;
editable?: boolean;
exportable?: boolean;
}

export interface ISidebarSection {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 45 additions & 5 deletions app/internal_packages/message-list/lib/message-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
GetMessageRFC2822Task,
Thread,
Message,
RegExpUtils,
} from 'mailspring-exports';
import { RetinaImg, ButtonDropdown, Menu } from 'mailspring-component-kit';
import { dialog } from '@electron/remote';

interface MessageControlsProps {
thread: Thread;
Expand Down Expand Up @@ -47,13 +49,24 @@ export default class MessageControls extends React.Component<MessageControlsProp
select: this._onShowOriginal,
};

const downloadEmail = {
name: localized('Download email'),
image: 'ic-dropdown-whitespace.png',
// TODO: Image isn't working - in this folders assets folder though
// image: 'mail-download-icon@2x.png',
select: this._onDownloadEmail,
};

if (!this.props.message.canReplyAll()) {
return [reply, forward, showOriginal];
return [reply, forward, showOriginal, downloadEmail];
}
const defaultReplyType = AppEnv.config.get('core.sending.defaultReplyType');
return defaultReplyType === 'reply-all'
? [replyAll, reply, forward, showOriginal]
: [reply, replyAll, forward, showOriginal];

return (defaultReplyType === 'reply-all' ? [replyAll, reply] : [reply, replyAll]).concat(
forward,
showOriginal,
downloadEmail
);
}

_dropdownMenu(items) {
Expand Down Expand Up @@ -121,7 +134,10 @@ export default class MessageControls extends React.Component<MessageControlsProp

_onShowOriginal = async () => {
const { message } = this.props;
const filepath = require('path').join(require('@electron/remote').app.getPath('temp'), message.id);
const filepath = require('path').join(
require('@electron/remote').app.getPath('temp'),
message.id
);
const task = new GetMessageRFC2822Task({
messageId: message.id,
accountId: message.accountId,
Expand All @@ -142,6 +158,30 @@ export default class MessageControls extends React.Component<MessageControlsProp
win.loadURL(`file://${filepath}`);
};

_onDownloadEmail = async () => {
const { message } = this.props;

const filepath = await dialog.showSaveDialog({
filters: [{ name: 'Email', extensions: ['eml'] }],
defaultPath: `${message.subject} - ${message.date.toLocaleString(
'sv-SE'
)} - ${message.id.substring(0, 10)}.eml`
.replace(/:/g, ';')
.replace(RegExpUtils.illegalPathCharacters(), '-')
.replace(RegExpUtils.unicodeControlCharacters(), '-'),
});

if (!filepath.canceled && filepath.filePath) {
Actions.queueTask(
new GetMessageRFC2822Task({
messageId: message.id,
accountId: message.accountId,
filepath: filepath.filePath,
})
);
}
};

_onLogData = () => {
console.log(this.props.message);
(window as any).__message = this.props.message;
Expand Down
2 changes: 2 additions & 0 deletions app/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"Do you prefer a single panel layout (like Gmail) or a two panel layout?": "Do you prefer a single panel layout (like Gmail) or a two panel layout?",
"Don’t show this again": "Don’t show this again",
"Download All": "Download All",
"Download Email": "Download Email",
"Download Failed": "Download Failed",
"Download Now": "Download Now",
"Downloading Update": "Downloading Update",
Expand Down Expand Up @@ -222,6 +223,7 @@
"Explore Mailspring Pro": "Explore Mailspring Pro",
"Export Failed": "Export Failed",
"Export Raw Data": "Export Raw Data",
"Export the %@ folder": "Export the %@ folder",
"Facebook URL": "Facebook URL",
"Failed to load \"%@\"": "Failed to load \"%@\"",
"Failed to load config.json: %@": "Failed to load config.json: %@",
Expand Down
22 changes: 19 additions & 3 deletions app/src/components/outline-view-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,11 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
};

_shouldShowContextMenu = () => {
return this.props.item.onDelete != null || this.props.item.onEdited != null;
return (
this.props.item.onDelete != null ||
this.props.item.onEdited != null ||
this.props.item.onExport != null
);
};

_shouldAcceptDrop = event => {
Expand Down Expand Up @@ -244,6 +248,10 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
}
};

_onExport = () => {
this._runCallback('onExport');
};

_onInputFocus = event => {
const input = event.target;
input.selectionStart = input.selectionEnd = input.value.length;
Expand Down Expand Up @@ -273,7 +281,7 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
if (this.props.item.onEdited) {
menu.append(
new MenuItem({
label: `${localized(`Rename`)} ${contextMenuLabel}`,
label: `${localized('Rename')} ${contextMenuLabel}`,
click: this._onEdit,
})
);
Expand All @@ -282,11 +290,19 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
if (this.props.item.onDelete) {
menu.append(
new MenuItem({
label: `${localized(`Delete`)} ${contextMenuLabel}`,
label: `${localized('Delete')} ${contextMenuLabel}`,
click: this._onDelete,
})
);
}

menu.append(
new MenuItem({
label: localized('Export the %@ folder', item.name ?? ''),
click: this._onExport,
})
);

menu.popup({});
};

Expand Down
5 changes: 3 additions & 2 deletions app/src/components/outline-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface IOutlineViewItem {
onSelect?: (...args: any[]) => any;
onDelete?: (...args: any[]) => any;
onEdited?: (...args: any[]) => any;
onExport?: (...args: any[]) => any;
}

interface OutlineViewProps {
Expand Down Expand Up @@ -184,15 +185,15 @@ export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {

_renderHeading(allowCreate, collapsed, collapsible) {
const collapseLabel = collapsed ? localized('Show') : localized('Hide');
let style: CSSProperties = {}
let style: CSSProperties = {};
if (this.props.titleColor) {
style = {
height: '50%',
paddingLeft: '4px',
borderLeftWidth: '4px',
borderLeftColor: this.props.titleColor,
borderLeftStyle: 'solid',
}
};
}
return (
<DropZone
Expand Down
6 changes: 6 additions & 0 deletions app/src/flux/models/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ export class Message extends ModelWithMetadata {
modelKey: 'folder',
itemClass: Folder,
}),

remoteFolderId: Attributes.String({
queryable: true,
modelKey: 'remoteFolderId',
}),
};

public subject: string;
Expand All @@ -198,6 +203,7 @@ export class Message extends ModelWithMetadata {
public replyToHeaderMessageId: string;
public forwardedHeaderMessageId: string;
public folder: Folder;
public remoteFolderId: string;

/** indicates that "body" is plain text, not HTML */
public plaintext: boolean;
Expand Down
14 changes: 14 additions & 0 deletions app/src/regexp-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,20 @@ const RegExpUtils = {
subcategorySplitRegex() {
return /[./\\]/g;
},

// Finds illegal path characters
// https://kb.acronis.com/content/39790
illegalPathCharacters() {
return /[/?<>\\:*|"]/g;
},

// Finds Unicode Control codes
// C0 0x00-0x1f & C1 (0x80-0x9f)
// http://en.wikipedia.org/wiki/C0_and_C1_control_codes
unicodeControlCharacters() {
// eslint-disable-next-line no-control-regex
return /[\x00-\x1f\x80-\x9f]/g;
},
};

export default RegExpUtils;