mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-12 22:28:32 +08:00
Download messages at a folder level
A few changes: - Right clicking on a folder (excluding Drafts, Unread, Starred) gives an option to "Export the %@ folder" - In order to get this to work I needed to add the property/reference to the existing DB column `remoteFolderId` - Sanitise regex utilities for paths - Downloading a single email now has it's name sanitised to stop saving errors (i.e slashes, colons, etc.) - Prettier/lint changes (for best practices) **Issues:** - Needs translations - long running process ties up mailsync - need to deprioritise tasks or have a `Action.queueBackgroundTask` option
This commit is contained in:
parent
aba6709e30
commit
1d6dd27dbd
8 changed files with 101 additions and 13 deletions
|
@ -11,10 +11,18 @@ import {
|
||||||
Actions,
|
Actions,
|
||||||
RegExpUtils,
|
RegExpUtils,
|
||||||
localized,
|
localized,
|
||||||
|
MessageStore,
|
||||||
|
GetMessageRFC2822Task,
|
||||||
|
MutableQuerySubscription,
|
||||||
|
Thread,
|
||||||
|
Message,
|
||||||
|
DatabaseStore,
|
||||||
|
Folder,
|
||||||
} from 'mailspring-exports';
|
} from 'mailspring-exports';
|
||||||
|
|
||||||
import * as SidebarActions from './sidebar-actions';
|
import * as SidebarActions from './sidebar-actions';
|
||||||
import { ISidebarItem } from './types';
|
import { ISidebarItem } from './types';
|
||||||
|
import { dialog } from '@electron/remote';
|
||||||
|
|
||||||
const idForCategories = categories => _.pluck(categories, 'id').join('-');
|
const idForCategories = categories => _.pluck(categories, 'id').join('-');
|
||||||
|
|
||||||
|
@ -74,6 +82,39 @@ const onDeleteItem = function (item) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
const onEditItem = function(item, value) {
|
||||||
let newDisplayName;
|
let newDisplayName;
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
@ -134,6 +175,7 @@ export default class SidebarItem {
|
||||||
counterStyle,
|
counterStyle,
|
||||||
onDelete: opts.deletable ? onDeleteItem : undefined,
|
onDelete: opts.deletable ? onDeleteItem : undefined,
|
||||||
onEdited: opts.editable ? onEditItem : undefined,
|
onEdited: opts.editable ? onEditItem : undefined,
|
||||||
|
onExport: opts.exportable ? onExportItem : undefined,
|
||||||
onCollapseToggled: toggleItemCollapsed,
|
onCollapseToggled: toggleItemCollapsed,
|
||||||
|
|
||||||
onDrop(item, event) {
|
onDrop(item, event) {
|
||||||
|
@ -190,6 +232,9 @@ export default class SidebarItem {
|
||||||
if (opts.editable == null) {
|
if (opts.editable == null) {
|
||||||
opts.editable = true;
|
opts.editable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opts.exportable = true;
|
||||||
|
|
||||||
opts.contextMenuLabel = contextMenuLabel;
|
opts.contextMenuLabel = contextMenuLabel;
|
||||||
return this.forPerspective(id, perspective, opts);
|
return this.forPerspective(id, perspective, opts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ export interface ISidebarItem {
|
||||||
|
|
||||||
deletable?: boolean;
|
deletable?: boolean;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
exportable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISidebarSection {
|
export interface ISidebarSection {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
GetMessageRFC2822Task,
|
GetMessageRFC2822Task,
|
||||||
Thread,
|
Thread,
|
||||||
Message,
|
Message,
|
||||||
|
RegExpUtils,
|
||||||
} from 'mailspring-exports';
|
} from 'mailspring-exports';
|
||||||
import { RetinaImg, ButtonDropdown, Menu } from 'mailspring-component-kit';
|
import { RetinaImg, ButtonDropdown, Menu } from 'mailspring-component-kit';
|
||||||
import { ipcRenderer, SaveDialogReturnValue } from 'electron';
|
import { ipcRenderer, SaveDialogReturnValue } from 'electron';
|
||||||
|
@ -162,10 +163,13 @@ export default class MessageControls extends React.Component<MessageControlsProp
|
||||||
const { message } = this.props;
|
const { message } = this.props;
|
||||||
|
|
||||||
const filepath = await dialog.showSaveDialog({
|
const filepath = await dialog.showSaveDialog({
|
||||||
filters: [{ name: 'Email', extensions: ['.eml'] }],
|
filters: [{ name: 'Email', extensions: ['eml'] }],
|
||||||
defaultPath: `${message.subject} - ${new Date().toLocaleDateString(
|
defaultPath: `${message.subject} - ${message.date.toLocaleString(
|
||||||
'sv-SE'
|
'sv-SE'
|
||||||
)} - ${message.id.substring(0, 10)}.eml`,
|
)} - ${message.id.substring(0, 10)}.eml`
|
||||||
|
.replace(/:/g, ';')
|
||||||
|
.replace(RegExpUtils.illegalPathCharacters(), '-')
|
||||||
|
.replace(RegExpUtils.unicodeControlCharacters(), '-'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!filepath.canceled && filepath.filePath) {
|
if (!filepath.canceled && filepath.filePath) {
|
||||||
|
|
|
@ -223,6 +223,7 @@
|
||||||
"Explore Mailspring Pro": "Explore Mailspring Pro",
|
"Explore Mailspring Pro": "Explore Mailspring Pro",
|
||||||
"Export Failed": "Export Failed",
|
"Export Failed": "Export Failed",
|
||||||
"Export Raw Data": "Export Raw Data",
|
"Export Raw Data": "Export Raw Data",
|
||||||
|
"Export the %@ folder": "Export the %@ folder",
|
||||||
"Facebook URL": "Facebook URL",
|
"Facebook URL": "Facebook URL",
|
||||||
"Failed to load \"%@\"": "Failed to load \"%@\"",
|
"Failed to load \"%@\"": "Failed to load \"%@\"",
|
||||||
"Failed to load config.json: %@": "Failed to load config.json: %@",
|
"Failed to load config.json: %@": "Failed to load config.json: %@",
|
||||||
|
|
|
@ -191,7 +191,11 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
|
||||||
};
|
};
|
||||||
|
|
||||||
_shouldShowContextMenu = () => {
|
_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 => {
|
_shouldAcceptDrop = event => {
|
||||||
|
@ -244,6 +248,10 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_onExport = () => {
|
||||||
|
this._runCallback('onExport');
|
||||||
|
};
|
||||||
|
|
||||||
_onInputFocus = event => {
|
_onInputFocus = event => {
|
||||||
const input = event.target;
|
const input = event.target;
|
||||||
input.selectionStart = input.selectionEnd = input.value.length;
|
input.selectionStart = input.selectionEnd = input.value.length;
|
||||||
|
@ -273,7 +281,7 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
|
||||||
if (this.props.item.onEdited) {
|
if (this.props.item.onEdited) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: `${localized(`Rename`)} ${contextMenuLabel}`,
|
label: `${localized('Rename')} ${contextMenuLabel}`,
|
||||||
click: this._onEdit,
|
click: this._onEdit,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -282,11 +290,19 @@ class OutlineViewItem extends Component<OutlineViewItemProps, OutlineViewItemSta
|
||||||
if (this.props.item.onDelete) {
|
if (this.props.item.onDelete) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: `${localized(`Delete`)} ${contextMenuLabel}`,
|
label: `${localized('Delete')} ${contextMenuLabel}`,
|
||||||
click: this._onDelete,
|
click: this._onDelete,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: localized('Export the %@ folder', item.name ?? ''),
|
||||||
|
click: this._onExport,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
menu.popup({});
|
menu.popup({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export interface IOutlineViewItem {
|
||||||
onSelect?: (...args: any[]) => any;
|
onSelect?: (...args: any[]) => any;
|
||||||
onDelete?: (...args: any[]) => any;
|
onDelete?: (...args: any[]) => any;
|
||||||
onEdited?: (...args: any[]) => any;
|
onEdited?: (...args: any[]) => any;
|
||||||
|
onExport?: (...args: any[]) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OutlineViewProps {
|
interface OutlineViewProps {
|
||||||
|
@ -184,7 +185,7 @@ export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
|
||||||
|
|
||||||
_renderHeading(allowCreate, collapsed, collapsible) {
|
_renderHeading(allowCreate, collapsed, collapsible) {
|
||||||
const collapseLabel = collapsed ? localized('Show') : localized('Hide');
|
const collapseLabel = collapsed ? localized('Show') : localized('Hide');
|
||||||
let style: CSSProperties = {}
|
let style: CSSProperties = {};
|
||||||
if (this.props.titleColor) {
|
if (this.props.titleColor) {
|
||||||
style = {
|
style = {
|
||||||
height: '50%',
|
height: '50%',
|
||||||
|
@ -192,7 +193,7 @@ export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
|
||||||
borderLeftWidth: '4px',
|
borderLeftWidth: '4px',
|
||||||
borderLeftColor: this.props.titleColor,
|
borderLeftColor: this.props.titleColor,
|
||||||
borderLeftStyle: 'solid',
|
borderLeftStyle: 'solid',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<DropZone
|
<DropZone
|
||||||
|
|
|
@ -177,6 +177,11 @@ export class Message extends ModelWithMetadata {
|
||||||
modelKey: 'folder',
|
modelKey: 'folder',
|
||||||
itemClass: Folder,
|
itemClass: Folder,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
remoteFolderId: Attributes.String({
|
||||||
|
queryable: true,
|
||||||
|
modelKey: 'remoteFolderId',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public subject: string;
|
public subject: string;
|
||||||
|
@ -198,6 +203,7 @@ export class Message extends ModelWithMetadata {
|
||||||
public replyToHeaderMessageId: string;
|
public replyToHeaderMessageId: string;
|
||||||
public forwardedHeaderMessageId: string;
|
public forwardedHeaderMessageId: string;
|
||||||
public folder: Folder;
|
public folder: Folder;
|
||||||
|
public remoteFolderId: string;
|
||||||
|
|
||||||
/** indicates that "body" is plain text, not HTML */
|
/** indicates that "body" is plain text, not HTML */
|
||||||
public plaintext: boolean;
|
public plaintext: boolean;
|
||||||
|
|
|
@ -317,6 +317,20 @@ const RegExpUtils = {
|
||||||
subcategorySplitRegex() {
|
subcategorySplitRegex() {
|
||||||
return /[./\\]/g;
|
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;
|
export default RegExpUtils;
|
||||||
|
|
Loading…
Add table
Reference in a new issue