summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.js2
-rw-r--r--app/imap.js66
-rw-r--r--app/src/App.vue16
-rw-r--r--app/src/components/Panel.vue40
-rw-r--r--app/src/pages/Home.vue66
-rw-r--r--app/src/store.js2
-rw-r--r--package-lock.json35
-rw-r--r--package.json3
-rw-r--r--webpack.config.js1
9 files changed, 167 insertions, 64 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index 0f0c91e..fc986f4 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -21,6 +21,6 @@ module.exports = {
rules: {
'comma-dangle': ['error', 'always-multiline'],
- 'vue/max-attributes-per-line': [1, { singleline: { max: 4 } }],
+ 'vue/max-attributes-per-line': [1, { singleline: { max: 5 } }],
},
};
diff --git a/app/imap.js b/app/imap.js
index 181d9cf..4041089 100644
--- a/app/imap.js
+++ b/app/imap.js
@@ -5,6 +5,13 @@ ipcMain.on('imap:listTree:from', listTreeFrom);
ipcMain.on('imap:listTree:to', listTreeTo);
ipcMain.on('imap:migrate', migrate);
+/**
+ * Connect to server
+ *
+ * @param {object} options
+ *
+ * @returns {Promise<ImapFlow>}
+ */
async function connect (options) {
const client = new ImapFlow({
host: options.server,
@@ -13,6 +20,7 @@ async function connect (options) {
user: options.username,
pass: options.password,
},
+ tls: options.tls,
});
await client.connect();
@@ -20,6 +28,15 @@ async function connect (options) {
return client;
}
+/**
+ * Turn fetch response into array
+ *
+ * @param {string} range
+ * @param {object} query
+ * @param {object} options
+ *
+ * @returns {Promise<*[]>}
+ */
ImapFlow.prototype.fetchArray = async function (range, query, options = {}) {
const msgsGenerator = await this.fetch(range, query, options);
const msgs = [];
@@ -30,45 +47,80 @@ ImapFlow.prototype.fetchArray = async function (range, query, options = {}) {
return msgs;
};
+/**
+ * @param {IpcMainEvent} event
+ * @param {object} options
+ */
async function listTreeFrom (event, options) {
- const client = await connect(options);
+ try {
+ const client = await connect(options);
- event.reply('imap:listTree:from:reply', await client.listTree());
- await client.logout();
+ event.reply('imap:listTree:from:reply', await client.listTree());
+ await client.logout();
+ } catch (e) {
+ event.reply('imap:from:error', e);
+ }
}
+/**
+ * @param {IpcMainEvent} event
+ * @param {object} options
+ */
async function listTreeTo (event, options) {
- const client = await connect(options);
+ try {
+ const client = await connect(options);
- event.reply('imap:listTree:to:reply', await client.listTree());
- await client.logout();
+ event.reply('imap:listTree:to:reply', await client.listTree());
+ await client.logout();
+ } catch (e) {
+ event.reply('imap:to:error', e);
+ }
}
+/**
+ * @param {IpcMainEvent} event
+ * @param {object} from
+ * @param {object} to
+ */
async function migrate (event, { from, to }) {
const fromClient = await connect(from);
const toClient = await connect(to);
+ event.reply('imap:migrate:progress', 'Getting folders');
const fromFolders = (await fromClient.list()).filter((folder) => folder.subscribed);
for (const fromFolder of fromFolders) {
+ event.reply('imap:migrate:progress', `Working on folder "${fromFolder.path}"`);
+
try {
await toClient.mailboxCreate(fromFolder.path);
} catch (e) {}
+ const appendCommands = [];
const fromMailbox = await fromClient.getMailboxLock(fromFolder.path);
const toMailbox = await toClient.getMailboxLock(fromFolder.path);
try {
+ event.reply('imap:migrate:progress', '- Collecting messages from target for comparison');
const toMsgs = await toClient.fetchArray('1:*', { flags: true, envelope: true, source: true });
+
+ event.reply('imap:migrate:progress', '- Collecting messages from source');
const fromMsgs = await fromClient.fetchArray('1:*', { flags: true, envelope: true, source: true });
+ 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`);
+
await toClient.noop();
for (const msg of msgs) {
- toClient.append(fromFolder.path, msg.source, Array.from(msg.flags), msg.envelope.date);
+ appendCommands.push(toClient.append(fromFolder.path, msg.source, Array.from(msg.flags), msg.envelope.date));
}
} finally {
+ await Promise.all(appendCommands);
+ event.reply('imap:migrate:progress', '- Done');
fromMailbox.release();
toMailbox.release();
}
}
+
+ event.reply('imap:migrate:progress', 'Done');
}
diff --git a/app/src/App.vue b/app/src/App.vue
index 98240ae..87365a2 100644
--- a/app/src/App.vue
+++ b/app/src/App.vue
@@ -1,3 +1,19 @@
<template>
<router-view />
</template>
+
+<script>
+import 'foundation-sites/dist/js/foundation.cjs';
+
+export default {};
+</script>
+
+<style lang="scss">
+@import '~foundation-sites/dist/css/foundation.min.css';
+
+body {
+ font-size: 1rem;
+ font-family: Helvetica, sans-serif;
+ padding: 1rem;
+}
+</style>
diff --git a/app/src/components/Panel.vue b/app/src/components/Panel.vue
index 3ae4000..527a8a9 100644
--- a/app/src/components/Panel.vue
+++ b/app/src/components/Panel.vue
@@ -1,21 +1,35 @@
<template>
- <div class="panel">
- <label>
- Server <input type="text" :value="modelValue.server" @input="emit('server', $event.target.value)">
- </label>
- <label>
- Port <input type="text" :value="modelValue.port" @input="emit('port', $event.target.value)">
- </label>
+ <form data-abide @submit.prevent="$emit('connect')">
+ <div class="grid-x">
+ <div class="cell">
+ <label>
+ Server <input type="text" :value="modelValue.server" required @input="emit('server', $event.target.value)">
+ </label>
+ </div>
+ </div>
+ <div class="grid-x">
+ <div class="cell">
+ <label>
+ Port <input type="text" :value="modelValue.port" required @input="emit('port', $event.target.value)">
+ </label>
+ </div>
+ </div>
<label>
Username <input type="text" :value="modelValue.username" @input="emit('username', $event.target.value)">
</label>
<label>
- Passwort <input type="text" :value="modelValue.password" @input="emit('password', $event.target.value)">
+ Passwort <input type="password" :value="modelValue.password" @input="emit('password', $event.target.value)">
</label>
- <button @click="$emit('connect')">
+ <div class="grid-x">
+ <div class="cell">
+ <input :id="`tls-${uid}`" type="checkbox" :checked="modelValue.tls" @input="emit('tls', $event.target.checked)">
+ <label :for="`tls-${uid}`">TLS enabled</label>
+ </div>
+ </div>
+ <button class="button" type="submit">
Connect
</button>
- </div>
+ </form>
</template>
<script>
@@ -27,6 +41,12 @@ export default {
],
emits: ['connect'],
+
+ data () {
+ return {
+ uid: crypto.getRandomValues(new Uint8Array(1)),
+ };
+ },
};
</script>
diff --git a/app/src/pages/Home.vue b/app/src/pages/Home.vue
index bfe92bf..8385a46 100644
--- a/app/src/pages/Home.vue
+++ b/app/src/pages/Home.vue
@@ -1,22 +1,38 @@
<template>
<div class="wrap">
<div class="panels">
- <Panel v-model="from" @connect="connectFrom" />
- <Panel v-model="to" @connect="connectTo" />
+ <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 @click="migrate">
+
+ <button class="button" @click="migrate">
Migrate!
</button>
- <h1>FROM</h1>
- <ul v-if="from.folders">
- <Folders :folder="from.folders" />
- </ul>
-
- <h1>TO</h1>
- <ul v-if="to.folders">
- <Folders :folder="to.folders" />
- </ul>
+ <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>
@@ -31,7 +47,10 @@ export default {
},
data () {
- return {};
+ return {
+ progress: '',
+ log: [],
+ };
},
computed: {
@@ -61,6 +80,18 @@ export default {
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: {
@@ -73,6 +104,9 @@ export default {
},
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)),
@@ -83,12 +117,6 @@ export default {
</script>
<style lang="scss">
-body {
- margin: 0;
- font-size: 1rem;
- font-family: Helvetica, sans-serif;
-}
-
.panels {
display: flex;
justify-content: space-between;
diff --git a/app/src/store.js b/app/src/store.js
index 818689b..294380c 100644
--- a/app/src/store.js
+++ b/app/src/store.js
@@ -8,12 +8,14 @@ export default createStore({
port: 3143,
username: 'from@example.org',
password: 'password',
+ tls: false,
},
to: {
server: 'localhost',
port: 31432,
username: 'to@example.org',
password: 'password',
+ tls: false,
},
};
},
diff --git a/package-lock.json b/package-lock.json
index cc3ce90..cc1cdf1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2524,11 +2524,6 @@
}
}
},
- "charenc": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
- "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
- },
"chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -2855,11 +2850,6 @@
"which": "^2.0.1"
}
},
- "crypt": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
- "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
- },
"crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -4337,6 +4327,11 @@
"mime-types": "^2.1.12"
}
},
+ "foundation-sites": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/foundation-sites/-/foundation-sites-6.6.3.tgz",
+ "integrity": "sha512-8X93wUAmUg1HhVv8uWMWnwoBLSQWSmFImJencneIZDctswn724Bq/MV1cbPZN/GFWGOB/9ngoQHztfzd4+ovCg=="
+ },
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -4882,11 +4877,6 @@
"call-bind": "^1.0.2"
}
},
- "is-buffer": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
- "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
- },
"is-callable": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
@@ -5111,6 +5101,11 @@
}
}
},
+ "jquery": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
+ "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
+ },
"js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
@@ -5443,16 +5438,6 @@
"escape-string-regexp": "^4.0.0"
}
},
- "md5": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
- "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
- "requires": {
- "charenc": "0.0.2",
- "crypt": "0.0.2",
- "is-buffer": "~1.1.6"
- }
- },
"meow": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
diff --git a/package.json b/package.json
index c202375..d78b675 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,9 @@
"dependencies": {
"core-js": "^3.15.0",
"electron": "^13.1.2",
+ "foundation-sites": "^6.6.3",
"imapflow": "^1.0.59",
- "md5": "^2.3.0",
+ "jquery": "^3.6.0",
"vue": "^3.1.1",
"vue-router": "^4.0.10",
"vuex": "^4.0.2"
diff --git a/webpack.config.js b/webpack.config.js
index a24703d..1ab2401 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -74,7 +74,6 @@ const config = {
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
- electron: require('electron'),
}),
new webpack.ExternalsPlugin('commonjs', ['electron']),
new MiniCssExtractPlugin(),