mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-28 23:44:38 +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,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();
|
||||
|
@ -28,7 +36,7 @@ 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 {
|
||||
|
@ -36,14 +44,14 @@ const isItemCollapsed = function (id) {
|
|||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface ISidebarItem {
|
|||
|
||||
deletable?: boolean;
|
||||
editable?: boolean;
|
||||
exportable?: boolean;
|
||||
}
|
||||
|
||||
export interface ISidebarSection {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
GetMessageRFC2822Task,
|
||||
Thread,
|
||||
Message,
|
||||
RegExpUtils,
|
||||
} from 'mailspring-exports';
|
||||
import { RetinaImg, ButtonDropdown, Menu } from 'mailspring-component-kit';
|
||||
import { ipcRenderer, SaveDialogReturnValue } from 'electron';
|
||||
|
@ -162,10 +163,13 @@ export default class MessageControls extends React.Component<MessageControlsProp
|
|||
const { message } = this.props;
|
||||
|
||||
const filepath = await dialog.showSaveDialog({
|
||||
filters: [{ name: 'Email', extensions: ['.eml'] }],
|
||||
defaultPath: `${message.subject} - ${new Date().toLocaleDateString(
|
||||
filters: [{ name: 'Email', extensions: ['eml'] }],
|
||||
defaultPath: `${message.subject} - ${message.date.toLocaleString(
|
||||
'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) {
|
||||
|
|
|
@ -223,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: %@",
|
||||
|
|
|
@ -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 => {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
@ -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({});
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -184,7 +185,7 @@ 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%',
|
||||
|
@ -192,7 +193,7 @@ export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
|
|||
borderLeftWidth: '4px',
|
||||
borderLeftColor: this.props.titleColor,
|
||||
borderLeftStyle: 'solid',
|
||||
}
|
||||
};
|
||||
}
|
||||
return (
|
||||
<DropZone
|
||||
|
|
|
@ -177,6 +177,11 @@ export class Message extends ModelWithMetadata {
|
|||
modelKey: 'folder',
|
||||
itemClass: Folder,
|
||||
}),
|
||||
|
||||
remoteFolderId: Attributes.String({
|
||||
queryable: true,
|
||||
modelKey: 'remoteFolderId',
|
||||
}),
|
||||
};
|
||||
|
||||
public subject: string;
|
||||
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue