1,662 changes: 1,628 additions & 34 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/node": "^14.0.14",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7",
"chalk": "^4.1.0",
"copy-webpack-plugin": "^6.1.0",
"eslint": "^8.20.0",
"eslint-config-google": "^0.14.0",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"jest": "^27.0.4",
Expand Down
4 changes: 2 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const SECTION_NAME = 'Email_Plugin';
export const EXPORT_TYPE = 'Export_Type';
export const ATTACHMENTS = 'Attachments'
export const PLUGIN_ICON = 'fa fa-envelope'
export const ATTACHMENTS = 'Attachments';
export const PLUGIN_ICON = 'fa fa-envelope';
18 changes: 9 additions & 9 deletions src/core/emailConfigure.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Login } from '../model/message.model'
import { emailProviders } from './emailProviders'
import { ImapConfig } from '../model/imapConfig.model';
import { EmailProvider } from 'src/model/emailProvider.model';
import {Login} from '../model/message.model';
import {emailProviders} from './emailProviders';
import {ImapConfig} from '../model/imapConfig.model';
import {EmailProvider} from 'src/model/emailProvider.model';

export function emailConfigure(login: Login): ImapConfig | undefined {

let user = login.email;
const password = login.password;

Expand All @@ -14,17 +13,18 @@ export function emailConfigure(login: Login): ImapConfig | undefined {
user = user.replace(/\s+/g, '');


const config: EmailProvider = emailProviders.find(({ type }) => user.includes('@' + type));
const config: EmailProvider = emailProviders.find(({type}) => user.includes('@' + type));

// In the case that the email provider is not present in the email providers list, it will be returned as "undefined".
if (!config)
// In the case that the email provider is not present in the email providers list, it will be returned as "undefined".
if (!config) {
return undefined;
}

const imapConfig: ImapConfig = {
user: user,
password: password,
...config,
}
};

return imapConfig;
}
6 changes: 3 additions & 3 deletions src/core/emailProviders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EmailProvider } from "../model/emailProvider.model";
import {EmailProvider} from '../model/emailProvider.model';

export const emailProviders: EmailProvider[] = [

Expand Down Expand Up @@ -49,6 +49,6 @@ export const emailProviders: EmailProvider[] = [
host: 'imap.yandex.com',
port: 993,
tls: true,
}
},

]
];
56 changes: 20 additions & 36 deletions src/core/imap.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,35 @@
import * as Imap from 'imap'
import { ImapConfig } from '../model/imapConfig.model';
import { Query } from '../model/Query.model';
import * as Imap from 'imap';
import {ImapConfig} from '../model/imapConfig.model';
import {Query} from '../model/Query.model';

export class IMAP {

private imap = null;
private monitorId = null;
private query: Query = null;
private delayTime = 1000 * 5;

constructor(config: ImapConfig) {

this.imap = new Imap({
...config,
authTimeout: 10000,
connTimeout: 30000,
tlsOptions: {
rejectUnauthorized: false
rejectUnauthorized: false,
},

});
}

init() {
return new Promise((resolve, reject) => {

this.imap.connect();

this.imap.once("ready", function () {
this.imap.once('ready', ()=> {
console.log('%c--------------------- SUCCESSFUL IMAP CONNECTION --------------------', 'color: Green');
resolve(this.imap);
});

this.imap.once('error', function (err: Error) {
this.imap.once('error', function(err: Error) {
reject(err);
});
});
Expand All @@ -44,12 +41,10 @@ export class IMAP {

openBox(mailBox, readOnly = true) {
return new Promise((resolve, reject) => {

this.imap.openBox(mailBox, readOnly, (err, box) => {
if (err) {
reject(err);
}
else {
} else {
resolve(box);
}
});
Expand All @@ -61,66 +56,56 @@ export class IMAP {
this.imap.search(criteria, (err, messages) => {
if (err) {
reject(err);
}
else {
} else {
resolve(messages);
}
});
});
}

state() {

return new Promise(async (resolve, reject) => {

if (!navigator.onLine) {
reject('No internet Connection')
}
else if (!this.imap)
reject('Please Re-login')
else if (this.imap.state == 'authenticated')
reject(new Error('No internet Connection'));
} else if (!this.imap) {
reject(new Error('Please Re-login'));
} else if (this.imap.state === 'authenticated') {
resolve('authenticated');
else {

} else {
try {
await this.init();
resolve('Reconnected successfully');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You resolve this passing string but never use the return value.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is where having type annotations could help, then it'd be immediately clear that you don't use the return type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You resolve this passing string but never use the return value.

I was trying to make the function more readable without comments.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is where having type annotations could help, then it'd be immediately clear that you don't use the return type.

Yes, I annotated the promises by void type.

}
catch (err) {
} catch (err) {
reject(err);
}
}
})
});
}

monitor() {

let mailBox = null;
let criteria = null;

this.monitorId = setInterval(async () => {
//
//
if (this.query) {

({ mailBox, criteria } = this.query);
({mailBox, criteria} = this.query);

try {
// Check if the connection is still stable.
await this.state();

await this.openBox(mailBox);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this returns box that you never use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done :)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does the change look like I cannot find anything in the latest commit?
It could be that I am just too much of coding novice to see it :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does the change look like I cannot find anything in the latest commit? It could be that I am just too much of coding novice to see it :)

Briefly, Dealing with operations in imap is asynchronous because we are waiting for a response from the mail service provider with approval or rejection, so this is not an instantaneous process.

That is why you will notice the word 'await' beside the function.

When this data is available from the email service provider, we use the 'resolve' and can also return the box or message from the email provider depending on what the function does.

But we don't need this data now, so what I did was just resolve() without value.

Before:
	resolve(box);
After:
	resolve();

If there is anything unclear, please tell me.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx I found the change in the code, eventually, in e8784fe, thx for pushing me in the right direction

thx for the nice explanation :), can you show me where resolve() is defined?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re welcome,

can you show me where resolve()

Inside promise function

let promise = new Promise(function(`resolve`, reject) {

});

Resolve and reject are callback functions, and calling the resolve function if the job is finished successfully.

If you are interested in the promise, this website is very nice.


let messages = await this.search(criteria);
const messages = await this.search(criteria);

console.log(messages);
}
catch (err) {
} catch (err) {
// Revoke the query
this.query = null;
alert(err);
}
}

}, this.delayTime);
}

Expand All @@ -129,5 +114,4 @@ export class IMAP {
this.imap.end();
console.log('%c--------------------- Close IMAP CONNECTION --------------------', 'color: Red');
}

}
}
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import App from './init';
const app = new App();

joplin.plugins.register({
onStart: async function () {
console.info('Email Plugin Started!');
await app.init();
},
onStart: async function() {
console.info('Email Plugin Started!');
await app.init();
},
});
21 changes: 8 additions & 13 deletions src/init.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import joplin from 'api';
import { ToolbarButtonLocation, MenuItemLocation } from 'api/types';
import { SECTION_NAME, PLUGIN_ICON } from './constants'
import { setting } from './setting';
import { Panel } from './ui/panel';
import {ToolbarButtonLocation, MenuItemLocation} from 'api/types';
import {SECTION_NAME, PLUGIN_ICON} from './constants';
import {setting} from './setting';
import {Panel} from './ui/panel';


export default class App {
panel: Panel
panel: Panel;

async init() {
await this.setupSetting();
Expand All @@ -17,18 +17,16 @@ export default class App {
}

async setupSetting() {

await joplin.settings.registerSection(SECTION_NAME, {
label: 'Email Plugin',
iconName: PLUGIN_ICON,
description: 'Fetch your important emails as notes.'
description: 'Fetch your important emails as notes.',
});

await joplin.settings.registerSettings(setting)
await joplin.settings.registerSettings(setting);
}

async setupToolbar() {

await joplin.commands.register({
name: 'toolBar',
label: 'Email Plugin',
Expand All @@ -42,8 +40,5 @@ export default class App {
// Two starting points.
await joplin.views.toolbarButtons.create('toolbarButton', 'toolBar', ToolbarButtonLocation.NoteToolbar);
await joplin.views.menuItems.create('menuItem', 'toolBar', MenuItemLocation.Tools);


}

}
}
2 changes: 1 addition & 1 deletion src/model/Query.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export interface Query {
mailBox: string,
criteria: string[]

}
}
2 changes: 1 addition & 1 deletion src/model/emailProvider.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export interface EmailProvider {
host: string,
port: number,
tls?: boolean,
}
}
2 changes: 1 addition & 1 deletion src/model/imapConfig.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ export interface ImapConfig {
port: number,
tls?: boolean,
tlsOptions?: object,
}
}
13 changes: 7 additions & 6 deletions src/model/message.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ImapConfig } from "./imapConfig.model";
import {ImapConfig} from './imapConfig.model';

export interface Login {
login: true,
Expand Down Expand Up @@ -45,8 +45,8 @@ export interface LoginManually extends ImapConfig {

// Type predicates
export function isLoginManually(message: any): message is LoginManually {
return 'login_manually' in message && 'user' in message && 'password' in message
&& 'host' in message && 'port' in message && 'tls' in message;
return 'login_manually' in message && 'user' in message && 'password' in message &&
'host' in message && 'port' in message && 'tls' in message;
}

export interface SearchByFrom {
Expand All @@ -56,10 +56,11 @@ export interface SearchByFrom {

// Type predicates
export function isSearchByFrom(message: any): message is SearchByFrom {
if (message.state == 'close')
if (message.state === 'close') {
return 'state' in message && 'from' in message;
else
} else {
return 'state' in message;
}
}

export type Message = Login | ManualConnection | Hide | LoginScreen | LoginManually | SearchByFrom;
export type Message = Login | ManualConnection | Hide | LoginScreen | LoginManually | SearchByFrom;
8 changes: 4 additions & 4 deletions src/setting.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SettingItemType } from 'api/types';
import { SECTION_NAME, EXPORT_TYPE, ATTACHMENTS } from './constants'
import {SettingItemType} from 'api/types';
import {SECTION_NAME, EXPORT_TYPE, ATTACHMENTS} from './constants';

export const setting = {

Expand All @@ -24,6 +24,6 @@ export const setting = {
public: true,
label: 'Include Attachments',

}
},

}
};
239 changes: 116 additions & 123 deletions src/ui/panel.ts
Original file line number Diff line number Diff line change
@@ -1,141 +1,134 @@
import joplin from 'api';
import JoplinViewsPanels from 'api/JoplinViewsPanels';
import { Message, Login, isLogin, isHide, isManualConnection, isLoginScreen, isLoginManually, isSearchByFrom, SearchByFrom } from '../model/message.model'
import { ImapConfig } from '../model/imapConfig.model';
import { emailConfigure } from '../core/emailConfigure';
import { IMAP } from '../core/imap';
import {Message, Login, isLogin, isHide, isManualConnection, isLoginScreen, isLoginManually, isSearchByFrom, SearchByFrom} from '../model/message.model';
import {ImapConfig} from '../model/imapConfig.model';
import {emailConfigure} from '../core/emailConfigure';
import {IMAP} from '../core/imap';


export class Panel {

panels: JoplinViewsPanels;
view: string;
visibility: boolean
account = null;
async setupPanel() {

if (this.view) {
await this.closeOpenPanel();
return;
}
this.panels = joplin.views.panels;

this.view = await this.panels.create('panel');

// Bootstrap v5.1.3.
await this.addScript('./ui/style/bootstrap.css');
await this.addScript('./ui/style/style.css');
await this.addScript('./ui/webview.js');

// display the login screen
await this.loginScreen();

await this.constructBridge();

}

async setHtml(html: string) {
await this.panels.setHtml(this.view, html);
}

async addScript(path: string) {
await this.panels.addScript(this.view, path);
}

async loginScreen() {
await this.setHtml(loginScreen)
}

async closeOpenPanel() {
this.visibility = await joplin.views.panels.visible(this.view);
await joplin.views.panels.show(this.view, !this.visibility);

}
// establishing a bridge between WebView and a plugin.
async constructBridge() {
await this.panels.onMessage(this.view, async (message: Message) => {
this.bridge(message)
});
}

async bridge(message: Message) {

switch (message !== undefined) {
// parsing email & start Imap eonnection
case isLogin(message):
const imapConfig: ImapConfig = emailConfigure(message as Login);

// It will alert the user using the manual connection if it can't find an email provider in the email providers list.
if (imapConfig) {
this.account = new IMAP(imapConfig as ImapConfig);

// If a connection is established, it will display the main screen and start monitoring waiting for any query.
try {
await this.account.init();
this.setHtml(mainScreen);
this.account.monitor();
} catch (err) {
alert(err);
throw new Error(err);
}

} else {
alert(`Sorry, an email provider couldn't be found, Please use the manual connection.`)
panels: JoplinViewsPanels;
view: string;
visibility: boolean;
account = null;
async setupPanel() {
if (this.view) {
await this.closeOpenPanel();
return;
}
break;
this.panels = joplin.views.panels;

case isLoginManually(message):
this.view = await this.panels.create('panel');

this.account = new IMAP(message as ImapConfig);
// Bootstrap v5.1.3.
await this.addScript('./ui/style/bootstrap.css');
await this.addScript('./ui/style/style.css');
await this.addScript('./ui/webview.js');

// If a connection is established, it will display the main screen and start monitoring waiting for any query.
try {
await this.account.init();
this.setHtml(mainScreen);
this.account.monitor();
} catch (err) {
alert(err);
throw new Error(err);
}
break;
// display the login screen
await this.loginScreen();

case isHide(message):
this.closeOpenPanel();
break;
await this.constructBridge();
}

case isManualConnection(message):
console.log('set Manual Screen');
this.setHtml(manualScreen);
break;
async setHtml(html: string) {
await this.panels.setHtml(this.view, html);
}

case isLoginScreen(message):
// close old account connection if any
if (this.account)
this.account.close(), this.account = null;
async addScript(path: string) {
await this.panels.addScript(this.view, path);
}

this.loginScreen();
break;
async loginScreen() {
await this.setHtml(loginScreen);
}

case isSearchByFrom(message):
let fromState: Message = message as SearchByFrom;
async closeOpenPanel() {
this.visibility = await joplin.views.panels.visible(this.view);
await joplin.views.panels.show(this.view, !this.visibility);
}
// establishing a bridge between WebView and a plugin.
async constructBridge() {
await this.panels.onMessage(this.view, async (message: Message) => {
this.bridge(message);
});
}

if (fromState.state == 'close') {
this.account.setQuery({
mailBox: 'inbox',
criteria: [['FROM', fromState.from]]
});
}
else {
this.account.setQuery(null);
async bridge(message: Message) {
switch (message !== undefined) {
// parsing email & start Imap eonnection
case isLogin(message):
const imapConfig: ImapConfig = emailConfigure(message as Login);

// It will alert the user using the manual connection if it can't find an email provider in the email providers list.
if (imapConfig) {
this.account = new IMAP(imapConfig as ImapConfig);

// If a connection is established, it will display the main screen and start monitoring waiting for any query.
try {
await this.account.init();
this.setHtml(mainScreen);
this.account.monitor();
} catch (err) {
alert(err);
throw new Error(err);
}
} else {
alert(`Sorry, an email provider couldn't be found, Please use the manual connection.`);
}
break;

case isLoginManually(message):

this.account = new IMAP(message as ImapConfig);

// If a connection is established, it will display the main screen and start monitoring waiting for any query.
try {
await this.account.init();
this.setHtml(mainScreen);
this.account.monitor();
} catch (err) {
alert(err);
throw new Error(err);
}
break;

case isHide(message):
this.closeOpenPanel();
break;

case isManualConnection(message):
console.log('set Manual Screen');
this.setHtml(manualScreen);
break;

case isLoginScreen(message):
// close old account connection if any
if (this.account) {
this.account.close(), this.account = null;
}

this.loginScreen();
break;

case isSearchByFrom(message):
const fromState: Message = message as SearchByFrom;

if (fromState.state === 'close') {
this.account.setQuery({
mailBox: 'inbox',
criteria: [['FROM', fromState.from]],
});
} else {
this.account.setQuery(null);
}
break;
}
break;
}
}

}


let loginScreen = `
const loginScreen = `
<div class="container">
<div class="row" style="font-size: large;">
Expand Down Expand Up @@ -183,9 +176,9 @@ let loginScreen = `
</div>
</div>
</div>
`
`;

let manualScreen = `
const manualScreen = `
<div class="container">
<div class="row" style="font-size: large;">
Expand Down Expand Up @@ -258,8 +251,8 @@ let manualScreen = `
</div>
</div>
</div>
`
let mainScreen = `
`;
const mainScreen = `
<div class="container">
<div class="row" style="font-size: large;">
Expand Down Expand Up @@ -306,4 +299,4 @@ let mainScreen = `
</div>
</div>
</div>
`
`;
35 changes: 15 additions & 20 deletions src/ui/webview.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,68 @@
/* eslint-disable no-unused-vars */

// When clicking on the 'login' button.
function login() {

const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;

webviewApi.postMessage({
login: true,
email: email,
password: password
password: password,
});

}

// When clicking on the 'Manually connect to IMAP' button.
function manualConnection() {

webviewApi.postMessage({
manual_connection: true,
});
}

// When clicking on the 'close' button.
function hide() {

webviewApi.postMessage({
hide: true,
});
}

// When clicking on the 'login screen' button.
function loginScreen() {

webviewApi.postMessage({
login_screen: true,
});
}

// When clicking on the 'login' button on the manual screen.
function loginManually() {

const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
const server = document.getElementById("server").value;
const ssl_tls = document.getElementById("ssl_tls").checked
const port = document.getElementById("port").value;
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const server = document.getElementById('server').value;
const sslTls = document.getElementById('ssl_tls').checked;
const port = document.getElementById('port').value;

webviewApi.postMessage({
login_manually: true,
user: email,
password: password,
host: server,
port: port,
tls: ssl_tls,
tls: sslTls,
});
}

function toggle() {
let from = document.getElementById('from').value;
let readOnly = document.getElementById('from').readOnly;
const from = document.getElementById('from').value;
const readOnly = document.getElementById('from').readOnly;

// It sends an email and then closes input.
if (!readOnly) {
webviewApi.postMessage({
state: 'close',
from: from
from: from,
});
document.getElementById('from').readOnly = !readOnly;
}
else {
} else {
webviewApi.postMessage({
state: 'open',
});
Expand Down
48 changes: 18 additions & 30 deletions test/emailConfigure.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { emailConfigure } from "../src/core/emailConfigure";
import { Login } from '../src/model/message.model';
import { ImapConfig } from '../src/model/imapConfig.model';
import {emailConfigure} from '../src/core/emailConfigure';
import {Login} from '../src/model/message.model';
import {ImapConfig} from '../src/model/imapConfig.model';


describe('Testing various types of writing the email', () => {

it('Uppercase email', () => {

const login: Login = {
login: true,
email: 'TEST@GMAIL.COM',
password: '12345'
}
password: '12345',
};

const config = emailConfigure(login);

Expand All @@ -22,19 +20,17 @@ describe('Testing various types of writing the email', () => {
host: 'imap.gmail.com',
port: 993,
tls: true,
}
};

expect(config).toStrictEqual(answer);

})
});

it('Email that starts with a space', () => {

const login: Login = {
login: true,
email: ' test@gmail.com',
password: '12345'
}
password: '12345',
};

const config = emailConfigure(login);

Expand All @@ -45,19 +41,17 @@ describe('Testing various types of writing the email', () => {
host: 'imap.gmail.com',
port: 993,
tls: true,
}
};

expect(config).toStrictEqual(answer);

});

it('Email that ends with a space', () => {

const login: Login = {
login: true,
email: 'test@outlook.com ',
password: '12345'
}
password: '12345',
};

const config = emailConfigure(login);

Expand All @@ -68,19 +62,17 @@ describe('Testing various types of writing the email', () => {
host: 'outlook.office365.com',
port: 993,
tls: true,
}
};

expect(config).toStrictEqual(answer);

});

it(`Presencing of a provider name from the provider's list within the username`, () => {

const login: Login = {
login: true,
email: 'gmail@aol.com',
password: '12345'
}
password: '12345',
};

const config = emailConfigure(login);

Expand All @@ -91,26 +83,22 @@ describe('Testing various types of writing the email', () => {
host: 'imap.aol.com',
port: 993,
tls: true,
}
};

expect(config).toStrictEqual(answer);

});

it('A provider that does not belong on the email provider list', () => {

const login: Login = {
login: true,
email: 'test@company.com',
password: '12345'
}
password: '12345',
};

const config = emailConfigure(login);

const answer = undefined;

expect(config).toStrictEqual(answer);

});

});