summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Weipert <code@drogueronin.de>2021-07-09 15:12:50 +0200
committerDaniel Weipert <code@drogueronin.de>2021-07-09 15:12:50 +0200
commit30c8c2f2b05bab8b962b51c0faeb282980324a5c (patch)
tree0088c91d7e44c76a966601d325bc53d1fdbc657e
parent6563602fb39db8ae9a976784b62538cbcf3de108 (diff)
Splits UI into steps and enhances log messages
-rw-r--r--app/imap.js49
-rw-r--r--app/index.html4
-rw-r--r--app/src/components/Folders.vue7
-rw-r--r--app/src/components/Panel.vue55
-rw-r--r--app/src/components/Select.vue81
-rw-r--r--app/src/pages/Home.vue129
-rw-r--r--app/src/pages/Steps/1-From.vue32
-rw-r--r--app/src/pages/Steps/2-To.vue51
-rw-r--r--app/src/pages/Steps/3-Migrate.vue111
-rw-r--r--app/src/router.js18
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
12 files changed, 394 insertions, 149 deletions
diff --git a/app/imap.js b/app/imap.js
index 4041089..d8caa0e 100644
--- a/app/imap.js
+++ b/app/imap.js
@@ -1,9 +1,10 @@
const { ipcMain } = require('electron');
const { ImapFlow } = require('imapflow');
-ipcMain.on('imap:listTree:from', listTreeFrom);
-ipcMain.on('imap:listTree:to', listTreeTo);
-ipcMain.on('imap:migrate', migrate);
+ipcMain.on('imap:connect', apiConnect);
+ipcMain.on('imap:listTree:from', apiListTreeFrom);
+ipcMain.on('imap:listTree:to', apiListTreeTo);
+ipcMain.on('imap:migrate', apiMigrate);
/**
* Connect to server
@@ -51,7 +52,22 @@ ImapFlow.prototype.fetchArray = async function (range, query, options = {}) {
* @param {IpcMainEvent} event
* @param {object} options
*/
-async function listTreeFrom (event, options) {
+async function apiConnect (event, options) {
+ try {
+ const client = await connect(options);
+
+ event.reply('imap:connect:reply', true);
+ await client.logout();
+ } catch (e) {
+ event.reply('imap:connect:error', e);
+ }
+}
+
+/**
+ * @param {IpcMainEvent} event
+ * @param {object} options
+ */
+async function apiListTreeFrom (event, options) {
try {
const client = await connect(options);
@@ -66,7 +82,7 @@ async function listTreeFrom (event, options) {
* @param {IpcMainEvent} event
* @param {object} options
*/
-async function listTreeTo (event, options) {
+async function apiListTreeTo (event, options) {
try {
const client = await connect(options);
@@ -82,7 +98,7 @@ async function listTreeTo (event, options) {
* @param {object} from
* @param {object} to
*/
-async function migrate (event, { from, to }) {
+async function apiMigrate (event, { from, to }) {
const fromClient = await connect(from);
const toClient = await connect(to);
@@ -108,17 +124,24 @@ async function migrate (event, { from, to }) {
event.reply('imap:migrate:progress', '- Comparing messages');
const msgs = fromMsgs.filter((fromMsg) => !toMsgs.some((toMsg) => Buffer.compare(toMsg.source, fromMsg.source) === 0));
- event.reply('imap:migrate:progress', `- Message status: Found ${fromMsgs.length} messages total and ${msgs.length} new messages to migrate`);
+ event.reply('imap:migrate:progress', `- Found ${fromMsgs.length} messages total and ${msgs.length} new messages to migrate`);
- await toClient.noop();
- for (const msg of msgs) {
- appendCommands.push(toClient.append(fromFolder.path, msg.source, Array.from(msg.flags), msg.envelope.date));
+ if (msgs.length > 0) {
+ event.reply('imap:migrate:progress', '- Starting migration');
+
+ await toClient.noop();
+ for (const msg of msgs) {
+ appendCommands.push(toClient.append(fromFolder.path, msg.source, Array.from(msg.flags), msg.envelope.date));
+ }
+
+ await Promise.all(appendCommands);
+ event.reply('imap:migrate:progress', '- Done');
}
- } finally {
- await Promise.all(appendCommands);
- event.reply('imap:migrate:progress', '- Done');
+
fromMailbox.release();
toMailbox.release();
+ } catch (e) {
+ event.reply('imap:migrate:error', e);
}
}
diff --git a/app/index.html b/app/index.html
index 8b7ddc9..df22432 100644
--- a/app/index.html
+++ b/app/index.html
@@ -2,8 +2,8 @@
<html>
<head>
<meta charset="UTF-8">
- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
- <title>Hello World!</title>
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; img-src data:">
+ <title>Mail Migrator</title>
<link rel="stylesheet" href="./build/index.css">
</head>
<body>
diff --git a/app/src/components/Folders.vue b/app/src/components/Folders.vue
index b801c1a..5c5c388 100644
--- a/app/src/components/Folders.vue
+++ b/app/src/components/Folders.vue
@@ -19,3 +19,10 @@ export default {
},
};
</script>
+
+<style lang="scss">
+ul {
+ padding-left: 1em;
+ list-style-type: dot;
+}
+</style>
diff --git a/app/src/components/Panel.vue b/app/src/components/Panel.vue
index 527a8a9..eb92af7 100644
--- a/app/src/components/Panel.vue
+++ b/app/src/components/Panel.vue
@@ -1,5 +1,9 @@
<template>
- <form data-abide @submit.prevent="$emit('connect')">
+ <form @submit.prevent="connect">
+ <div v-show="apiMessage.type" class="callout" :class="{ success: apiMessage.type === 'success', alert: apiMessage.type === 'error' }">
+ <p>{{ apiMessage.msg }}</p>
+ </div>
+
<div class="grid-x">
<div class="cell">
<label>
@@ -10,7 +14,14 @@
<div class="grid-x">
<div class="cell">
<label>
- Port <input type="text" :value="modelValue.port" required @input="emit('port', $event.target.value)">
+ Port
+ <Select
+ :model-value="modelValue.port"
+ required
+ :options="ports"
+ :config="{ tags: true }"
+ @update:modelValue="(value) => emit('port', value)"
+ />
</label>
</div>
</div>
@@ -33,9 +44,14 @@
</template>
<script>
-import dynamicModelObjectEmit from '../mixins/dynamicModelObjectEmit';
+import Select from './Select';
+import dynamicModelObjectEmit from '~/mixins/dynamicModelObjectEmit';
export default {
+ components: {
+ Select,
+ },
+
mixins: [
dynamicModelObjectEmit,
],
@@ -45,8 +61,41 @@ export default {
data () {
return {
uid: crypto.getRandomValues(new Uint8Array(1)),
+ apiMessage: {
+ type: '',
+ msg: '',
+ },
+
+ ports: [...new Set([
+ 143,
+ 993,
+ Number(this.modelValue.port),
+ ])].map((port) => ({ id: port, text: port })),
};
},
+
+ mounted () {
+ this.$electron.ipcRenderer.on('imap:connect:reply', (event, success) => {
+ this.apiMessage = {
+ type: 'success',
+ msg: 'Connected!',
+ };
+ });
+
+ this.$electron.ipcRenderer.on('imap:connect:error', (event, error) => {
+ this.apiMessage = {
+ type: 'error',
+ msg: error,
+ };
+ });
+ },
+
+ methods: {
+ connect () {
+ this.$electron.ipcRenderer.send('imap:connect', JSON.parse(JSON.stringify(this.modelValue)));
+ this.$emit('connect');
+ },
+ },
};
</script>
diff --git a/app/src/components/Select.vue b/app/src/components/Select.vue
new file mode 100644
index 0000000..f2d1187
--- /dev/null
+++ b/app/src/components/Select.vue
@@ -0,0 +1,81 @@
+<template>
+ <select />
+</template>
+
+<script>
+import 'select2/dist/js/select2.min';
+import $ from 'jquery';
+
+export default {
+ props: {
+ options: {
+ type: Array,
+ required: true,
+ },
+
+ modelValue: {
+ type: [Number, String],
+ required: true,
+ },
+
+ config: {
+ type: Object,
+ default: () => ({}),
+ },
+ },
+
+ emits: ['update:modelValue'],
+
+ watch: {
+ options (value) {
+ $(this.$el)
+ .empty().select2({ data: value });
+ },
+
+ modelValue (value) {
+ $(this.$el)
+ .val(value)
+ .trigger('change');
+ },
+ },
+
+ mounted () {
+ $(this.$el)
+ .select2({ ...this.config, data: this.options })
+ .val(this.modelValue)
+ .trigger('change')
+ .on('change', (ev) => {
+ this.$emit('update:modelValue', ev.target.value);
+ });
+ },
+
+ unmounted () {
+ $(this.$el)
+ .off()
+ .select2('destroy');
+ },
+};
+</script>
+
+<style lang="scss">
+@import '~select2/dist/css/select2.min.css';
+
+.select2 {
+ &-selection {
+ height: 2.4375rem !important;
+ border-radius: 0 !important;
+ border: 1px solid #cacaca !important;
+
+ &__rendered {
+ padding: .5rem;
+ font-size: 1rem !important;
+ line-height: 1.5 !important;
+ }
+
+ &__arrow {
+ height: 2.4375rem !important;
+ top: 0;
+ }
+ }
+}
+</style>
diff --git a/app/src/pages/Home.vue b/app/src/pages/Home.vue
deleted file mode 100644
index 8385a46..0000000
--- a/app/src/pages/Home.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-<template>
- <div class="wrap">
- <div class="panels">
- <div>
- <h1>FROM</h1>
- {{ from.error }}
- <Panel v-model="from" @connect="connectFrom" />
- <ul v-if="from.folders">
- <Folders :folder="from.folders" />
- </ul>
- </div>
-
- <div>
- <h1>TO</h1>
- {{ to.error }}
- <Panel v-model="to" @connect="connectTo" />
- <ul v-if="to.folders">
- <Folders :folder="to.folders" />
- </ul>
- </div>
- </div>
-
- <button class="button" @click="migrate">
- Migrate!
- </button>
-
- <div class="progress-screen">
- <b>{{ progress }}</b>
- <br>
- <div class="progress-screen__log">
- <div v-for="(msg, idx) in log" :key="idx">
- {{ msg }}
- </div>
- </div>
- </div>
- </div>
-</template>
-
-<script>
-import Panel from '../components/Panel.vue';
-import Folders from '../components/Folders';
-
-export default {
- components: {
- Panel,
- Folders,
- },
-
- data () {
- return {
- progress: '',
- log: [],
- };
- },
-
- computed: {
- from: {
- set (value) {
- this.$store.commit('setFrom', value);
- },
- get () {
- return this.$store.state.from;
- },
- },
- to: {
- set (value) {
- this.$store.commit('setTo', value);
- },
- get () {
- return this.$store.state.to;
- },
- },
- },
-
- mounted () {
- this.$electron.ipcRenderer.on('imap:listTree:from:reply', (event, folders) => {
- this.from.folders = folders;
- });
-
- this.$electron.ipcRenderer.on('imap:listTree:to:reply', (event, folders) => {
- this.to.folders = folders;
- });
-
- this.$electron.ipcRenderer.on('imap:from:error', (event, error) => {
- this.from.error = error;
- });
- this.$electron.ipcRenderer.on('imap:to:error', (event, error) => {
- this.to.error = error;
- });
-
- this.$electron.ipcRenderer.on('imap:migrate:progress', (event, progress) => {
- this.progress = progress;
- this.log.push(progress);
- });
- },
-
- methods: {
- connectFrom () {
- this.$electron.ipcRenderer.send('imap:listTree:from', JSON.parse(JSON.stringify(this.from)));
- },
-
- connectTo () {
- this.$electron.ipcRenderer.send('imap:listTree:to', JSON.parse(JSON.stringify(this.to)));
- },
-
- async migrate () {
- this.progress = '';
- this.log = [];
-
- this.$electron.ipcRenderer.send('imap:migrate', {
- from: JSON.parse(JSON.stringify(this.from)),
- to: JSON.parse(JSON.stringify(this.to)),
- });
- },
- },
-};
-</script>
-
-<style lang="scss">
-.panels {
- display: flex;
- justify-content: space-between;
-}
-
-ul {
- padding-left: 1em;
- list-style-type: dot;
-}
-</style>
diff --git a/app/src/pages/Steps/1-From.vue b/app/src/pages/Steps/1-From.vue
new file mode 100644
index 0000000..e1bd82e
--- /dev/null
+++ b/app/src/pages/Steps/1-From.vue
@@ -0,0 +1,32 @@
+<template>
+ <div>
+ <h1>From</h1>
+
+ <Panel v-model="from" />
+
+ <router-link to="/steps/2">
+ next
+ </router-link>
+ </div>
+</template>
+
+<script>
+import Panel from '~/components/Panel';
+
+export default {
+ components: {
+ Panel,
+ },
+
+ computed: {
+ from: {
+ set (value) {
+ this.$store.commit('setFrom', value);
+ },
+ get () {
+ return this.$store.state.from;
+ },
+ },
+ },
+};
+</script>
diff --git a/app/src/pages/Steps/2-To.vue b/app/src/pages/Steps/2-To.vue
new file mode 100644
index 0000000..1cce01d
--- /dev/null
+++ b/app/src/pages/Steps/2-To.vue
@@ -0,0 +1,51 @@
+<template>
+ <div>
+ <router-link to="/steps/1">
+ previous
+ </router-link>
+
+ <h1>To</h1>
+
+ <Panel v-model="to" @connect="connect" />
+
+ <router-link to="/steps/3">
+ next
+ </router-link>
+ </div>
+</template>
+
+<script>import Panel from '~/components/Panel';
+
+export default {
+ components: {
+ Panel,
+ },
+
+ computed: {
+ to: {
+ set (value) {
+ this.$store.commit('setTo', value);
+ },
+ get () {
+ return this.$store.state.to;
+ },
+ },
+ },
+
+ mounted () {
+ this.$electron.ipcRenderer.on('imap:listTree:to:reply', (event, folders) => {
+ this.to.folders = folders;
+ });
+
+ this.$electron.ipcRenderer.on('imap:to:error', (event, error) => {
+ this.to.error = error;
+ });
+ },
+
+ methods: {
+ connect () {
+ this.$electron.ipcRenderer.send('imap:listTree:to', JSON.parse(JSON.stringify(this.to)));
+ },
+ },
+};
+</script>
diff --git a/app/src/pages/Steps/3-Migrate.vue b/app/src/pages/Steps/3-Migrate.vue
new file mode 100644
index 0000000..c8d2e39
--- /dev/null
+++ b/app/src/pages/Steps/3-Migrate.vue
@@ -0,0 +1,111 @@
+<template>
+ <div>
+ <router-link to="/steps/2">
+ previous
+ </router-link>
+
+ <div>
+ From
+ <div class="input-group">
+ <input type="text" disabled :value="from.server"> <input type="text" disabled :value="from.port">
+ </div>
+ </div>
+ <div>
+ To
+ <div class="input-group">
+ <input type="text" disabled :value="to.server"> <input type="text" disabled :value="to.port">
+ </div>
+ </div>
+
+ <div>
+ Bedingungen
+ <br>
+ <input type="text">
+ </div>
+
+ <ul v-if="from.folders">
+ <Folders :folder="from.folders" />
+ </ul>
+
+ <button class="button" @click="migrate">
+ Migrate!
+ </button>
+
+ <div class="progress-screen">
+ <b>{{ progress }}</b>
+ <br>
+ <b>{{ progressMessages }}</b>
+ <br>
+ <div class="progress-screen__log">
+ <div v-for="(msg, idx) in log" :key="idx">
+ {{ msg }}
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import Folders from '~/components/Folders';
+
+export default {
+ components: {
+ Folders,
+ },
+
+ data () {
+ return {
+ progress: '',
+ log: [],
+ progressMessages: '',
+ };
+ },
+
+ computed: {
+ from: {
+ set (value) {
+ this.$store.commit('setFrom', value);
+ },
+ get () {
+ return this.$store.state.from;
+ },
+ },
+ to: {
+ set (value) {
+ this.$store.commit('setTo', value);
+ },
+ get () {
+ return this.$store.state.to;
+ },
+ },
+ },
+
+ mounted () {
+ this.$electron.ipcRenderer.on('imap:listTree:from:reply', (event, folders) => {
+ this.from.folders = folders;
+ });
+ this.$electron.ipcRenderer.send('imap:listTree:from', JSON.parse(JSON.stringify(this.from)));
+
+ this.$electron.ipcRenderer.on('imap:migrate:progress', (event, progress) => {
+ this.progress = progress;
+ this.log.push(progress);
+ });
+ this.$electron.ipcRenderer.on('imap:migrate:progress:messages', (event, progress) => {
+ this.progressMessages = progress;
+ });
+ },
+
+ methods: {
+ async migrate () {
+ this.progress = '';
+ this.log = [];
+ this.progressMessages = '';
+
+ this.$electron.ipcRenderer.send('imap:migrate', JSON.parse(JSON.stringify({
+ from: this.from,
+ to: this.to,
+ })));
+ },
+ },
+};
+</script>
diff --git a/app/src/router.js b/app/src/router.js
index 7676449..93ca201 100644
--- a/app/src/router.js
+++ b/app/src/router.js
@@ -1,12 +1,26 @@
import { createRouter, createWebHashHistory } from 'vue-router';
-import Home from './pages/Home';
+import Step1 from './pages/Steps/1-From';
+import Step2 from './pages/Steps/2-To';
+import Step3 from './pages/Steps/3-Migrate';
export default createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
- component: Home,
+ redirect: '/steps/1',
+ },
+ {
+ path: '/steps/1',
+ component: Step1,
+ },
+ {
+ path: '/steps/2',
+ component: Step2,
+ },
+ {
+ path: '/steps/3',
+ component: Step3,
},
],
});
diff --git a/package-lock.json b/package-lock.json
index cc1cdf1..9f48f98 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6726,6 +6726,11 @@
}
}
},
+ "select2": {
+ "version": "4.1.0-rc.0",
+ "resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz",
+ "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A=="
+ },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
diff --git a/package.json b/package.json
index d78b675..6d9a7c9 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"foundation-sites": "^6.6.3",
"imapflow": "^1.0.59",
"jquery": "^3.6.0",
+ "select2": "^4.1.0-rc.0",
"vue": "^3.1.1",
"vue-router": "^4.0.10",
"vuex": "^4.0.2"