From aba6709e30fcd4ce34ecfbaa45e385862b2d5f70 Mon Sep 17 00:00:00 2001 From: Glenn <33450392+glenn2223@users.noreply.github.com> Date: Tue, 23 Aug 2022 16:51:21 +0100 Subject: [PATCH 1/3] Download an email An option is available in the reply drop-down called "Download Email", clicking this will prompt the user for the output directory --- .../assets/mail-download-icon.png | Bin 0 -> 601 bytes .../assets/mail-download-icon@2x.png | Bin 0 -> 1122 bytes .../message-list/lib/message-controls.tsx | 47 ++++++++++++++++-- app/lang/en.json | 1 + 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 app/internal_packages/message-list/assets/mail-download-icon.png create mode 100644 app/internal_packages/message-list/assets/mail-download-icon@2x.png diff --git a/app/internal_packages/message-list/assets/mail-download-icon.png b/app/internal_packages/message-list/assets/mail-download-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03991755dba37dc767a8b284d451034398f07b90 GIT binary patch literal 601 zcmV-f0;c_mP)wu=9EIc6J@7 zmRkN;?utpj-w!YZH}aFM*Xw0z0C3*M@Nwg0X9(g&E;cxNVtoSLUFcBRTxhntrav{r zcLE(iBj~6Mwqrs>3+I)IxZ-;b?Ll*80EU!$CpsTu*g$<~j3hd=XBB{h4Jn2WBmXwW zjBK1o%-kV@lVf{a(;_xzj68<68N{H%u?JmYI#!HVDB70Kt@@D}g#27FLsPvsLM5;il-RxEi)Uo{h>3N6-CxW5e!u^0wOVhOK0qH$??Rrex$#kYP2nRZ nAg2`BcJhv?rIvpqUjhsOya3AVL}m>u00000NkvXXu0mjfn8F8^ literal 0 HcmV?d00001 diff --git a/app/internal_packages/message-list/assets/mail-download-icon@2x.png b/app/internal_packages/message-list/assets/mail-download-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..55fbc3bb2532a5b23eac1538883917a733f688cf GIT binary patch literal 1122 zcmV-o1fBbdP)|fYI=MjQ zty^p$d+i%oCx~-`Bqy+MV0r9hck2+q>ItM=Akh)htz-FuPlftIiKI*^Hp&km;vbsi z_xS!5Av`=hJUl!+JUl%9OAPo2S65e|{Iihb@&x&89v&W&)&c zw#44w%8mE%6YoiW3O`lVR=Vw-YXNNJ7%^2E%khjGcn|WVfm?R-DbD%mS^z?-e2&F; z%=R*Qm_?7oLpez0Z`oJ(@{AKXUb--WQV1_{%xE+A)sha?wMoFElgiI49~0r33!fC2 zPQ+#tZ2mXo(BZl=3ArwrT*g8(X(iqI&cz;q&1AVL+1oknu0#YO%pH@T70(kIdvJG^glY z1E9uPLv@ywp%V7KSCB7-m@~&n+Vxx!m}-wPGR0@qEKzR)05Kl6VvH&%RBjbbmmSQf zsK)t+cjxmmjC z^eCXLT+jL-ujnx}mATzX*raWd;s}Yx zNfx61;f{44uskyIH`u@K3}pv9b|nDOGkKj!eJ<;M1<9&U$_>`C$^&S@O@}sGpoY)o z9=dE!pf8xn*C{7osWL+ipIQMFHNIfqv0cX%2RqgY0p?JY873>Sby;LuYXVTOq-Wc% z65g2P^=P++hP{Y?$n)qmXee=qJqlq36%Nt(2n|hhg5=D!a(q*)4aAWFCAR7F1)H#! zB6aSjCFTm~|EFz|8cl?WJ8Ow`fSG8~<8MG**C;(o0l-mB90;LovDZFG{cxljAfBF{ zeqUT%e3LIf%JHSe5+poo#s6rUG!Q2MBw^P=-9}eA#(UHM7d#08u#V9fX>r6zKf=@J o!o$PE!^6YF!^6Wvhd%-g0Gr6x<$p-*>i_@%07*qoM6N<$f+l+dga7~l literal 0 HcmV?d00001 diff --git a/app/internal_packages/message-list/lib/message-controls.tsx b/app/internal_packages/message-list/lib/message-controls.tsx index dccf7e70f1..68d42208b1 100644 --- a/app/internal_packages/message-list/lib/message-controls.tsx +++ b/app/internal_packages/message-list/lib/message-controls.tsx @@ -11,6 +11,8 @@ import { Message, } from 'mailspring-exports'; import { RetinaImg, ButtonDropdown, Menu } from 'mailspring-component-kit'; +import { ipcRenderer, SaveDialogReturnValue } from 'electron'; +import { dialog } from '@electron/remote'; interface MessageControlsProps { thread: Thread; @@ -47,13 +49,24 @@ export default class MessageControls extends React.Component { 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, @@ -142,6 +158,27 @@ export default class MessageControls extends React.Component { + const { message } = this.props; + + const filepath = await dialog.showSaveDialog({ + filters: [{ name: 'Email', extensions: ['.eml'] }], + defaultPath: `${message.subject} - ${new Date().toLocaleDateString( + 'sv-SE' + )} - ${message.id.substring(0, 10)}.eml`, + }); + + 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; diff --git a/app/lang/en.json b/app/lang/en.json index 1569614c8b..83fb9e1a70 100644 --- a/app/lang/en.json +++ b/app/lang/en.json @@ -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", From 1d6dd27dbd39a6c20ed30a588cdacf028fa59c4a Mon Sep 17 00:00:00 2001 From: Glenn <33450392+glenn2223@users.noreply.github.com> Date: Thu, 25 Aug 2022 12:49:49 +0100 Subject: [PATCH 2/3] 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 --- .../account-sidebar/lib/sidebar-item.ts | 55 +++++++++++++++++-- .../account-sidebar/lib/types.ts | 1 + .../message-list/lib/message-controls.tsx | 10 +++- app/lang/en.json | 1 + app/src/components/outline-view-item.tsx | 22 +++++++- app/src/components/outline-view.tsx | 5 +- app/src/flux/models/message.ts | 6 ++ app/src/regexp-utils.ts | 14 +++++ 8 files changed, 101 insertions(+), 13 deletions(-) diff --git a/app/internal_packages/account-sidebar/lib/sidebar-item.ts b/app/internal_packages/account-sidebar/lib/sidebar-item.ts index 6a412eb2c0..ddbb219ff7 100644 --- a/app/internal_packages/account-sidebar/lib/sidebar-item.ts +++ b/app/internal_packages/account-sidebar/lib/sidebar-item.ts @@ -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, { 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); } diff --git a/app/internal_packages/account-sidebar/lib/types.ts b/app/internal_packages/account-sidebar/lib/types.ts index 989ffe17d2..c3b01d43c7 100644 --- a/app/internal_packages/account-sidebar/lib/types.ts +++ b/app/internal_packages/account-sidebar/lib/types.ts @@ -20,6 +20,7 @@ export interface ISidebarItem { deletable?: boolean; editable?: boolean; + exportable?: boolean; } export interface ISidebarSection { diff --git a/app/internal_packages/message-list/lib/message-controls.tsx b/app/internal_packages/message-list/lib/message-controls.tsx index 68d42208b1..931aad53ec 100644 --- a/app/internal_packages/message-list/lib/message-controls.tsx +++ b/app/internal_packages/message-list/lib/message-controls.tsx @@ -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 { - 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 { + this._runCallback('onExport'); + }; + _onInputFocus = event => { const input = event.target; input.selectionStart = input.selectionEnd = input.value.length; @@ -273,7 +281,7 @@ class OutlineViewItem extends Component any; onDelete?: (...args: any[]) => any; onEdited?: (...args: any[]) => any; + onExport?: (...args: any[]) => any; } interface OutlineViewProps { @@ -184,7 +185,7 @@ export class OutlineView extends Component { _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 { borderLeftWidth: '4px', borderLeftColor: this.props.titleColor, borderLeftStyle: 'solid', - } + }; } 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; From c40d6fae6c798a504f675349747b7f89ea3b567a Mon Sep 17 00:00:00 2001 From: Glenn <33450392+glenn2223@users.noreply.github.com> Date: Thu, 25 Aug 2022 17:04:44 +0100 Subject: [PATCH 3/3] Drop redundant imports --- app/internal_packages/message-list/lib/message-controls.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/internal_packages/message-list/lib/message-controls.tsx b/app/internal_packages/message-list/lib/message-controls.tsx index 931aad53ec..910fe8c281 100644 --- a/app/internal_packages/message-list/lib/message-controls.tsx +++ b/app/internal_packages/message-list/lib/message-controls.tsx @@ -12,7 +12,6 @@ import { RegExpUtils, } from 'mailspring-exports'; import { RetinaImg, ButtonDropdown, Menu } from 'mailspring-component-kit'; -import { ipcRenderer, SaveDialogReturnValue } from 'electron'; import { dialog } from '@electron/remote'; interface MessageControlsProps {