diff options
-rw-r--r-- | .eslintrc.js | 2 | ||||
-rw-r--r-- | app/imap.js | 66 | ||||
-rw-r--r-- | app/src/App.vue | 16 | ||||
-rw-r--r-- | app/src/components/Panel.vue | 40 | ||||
-rw-r--r-- | app/src/pages/Home.vue | 66 | ||||
-rw-r--r-- | app/src/store.js | 2 | ||||
-rw-r--r-- | package-lock.json | 35 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | webpack.config.js | 1 |
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(), |